diff --git a/.gitignore b/.gitignore index 1b7f964..46ba0f5 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,4 @@ lcov.info .vim .vscode +uv.lock diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 9d709ab..a2d738f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,6 +18,7 @@ build: - echo "Command run at 'post_build' step" - echo `date` + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py diff --git a/docs/source/conf.py b/docs/source/conf.py index ef13ef5..d0cf09c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,19 +4,18 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import importlib -import sys import os +import sys sys.path.insert(0, os.path.abspath("../..")) print(sys.path) -import ptulsconv # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'ptulsconv' -copyright = '2019-2025 Jamie Hardt. All rights reserved' +project = "ptulsconv" +copyright = "2019-2025 Jamie Hardt. All rights reserved" version = "Version 2" release = importlib.metadata.version("ptulsconv") @@ -24,31 +23,29 @@ release = importlib.metadata.version("ptulsconv") # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', + "sphinx.ext.autodoc", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", ] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] -master_doc = 'index' +master_doc = "index" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] - +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] latex_documents = [ - (master_doc, 'ptulsconv.tex', u'ptulsconv Documentation', - u'Jamie Hardt', 'manual'), + (master_doc, "ptulsconv.tex", "ptulsconv Documentation", "Jamie Hardt", "manual"), ] @@ -67,7 +64,7 @@ epub_title = project # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index d94435c..8a7e27a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ptulsconv" -version = "4.0.0" +version = "4.0.1" description = "Read Pro Tools Text exports and generate PDF ADR Reports, JSON" readme = "README.md" requires-python = ">=3.9" @@ -9,7 +9,6 @@ license-files = ["LICENSE"] keywords = ["text-processing", "parsers", "film", "broadcast", "editing", "editorial"] classifiers = [ - 'License :: OSI Approved :: MIT License', 'Topic :: Multimedia', 'Topic :: Multimedia :: Sound/Audio', "Programming Language :: Python :: 3.9", @@ -42,17 +41,24 @@ doc = [ "sphinx-rtd-theme>=3.0.2", ] +[tool.ruff.lint] +ignore = [ + "DTZ005" # datetime created without a tz +] + +[tool.ruff.lint.per-file-ignores] +"reaper/Export Items as Text.py" = ["F821"] [project.scripts] ptulsconv = "ptulsconv:__main__.main" [build-system] -requires = ["uv_build>=0.8.18,<0.9.0"] +requires = ["uv_build>=0.8.18,<0.12.0"] build-backend = "uv_build" [dependency-groups] dev = [ - "ruff==0.13.1", + "ruff>=0.16.8", ] doc = [ "sphinx>=7.4.7", diff --git a/reaper/Export Items as Text.py b/reaper/Export Items as Text.py index fce6828..904dad9 100644 --- a/reaper/Export Items as Text.py +++ b/reaper/Export Items as Text.py @@ -1,47 +1,68 @@ +# type: ignore[reportUndefinedVariable] + # Export Items as Text.py # (c) 2021 Jamie Hardt. All rights reserved. # # - -import json -import os.path import datetime +import json +from zoneinfo import ZoneInfo -item_records = list() +item_records = [] -for i in range(0, RPR_CountMediaItems(0) ): +for i in range(RPR_CountMediaItems(0)): this_item = RPR_GetMediaItem(0, i) item_record = {} - item_record["mute"] = True if RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0. else False - + item_record["mute"] = RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0 + item_record["duration"] = RPR_GetMediaItemInfo_Value(this_item, "D_LENGTH") - _, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len(item_record["duration"], "", 128, 0., 5) + _, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len( + item_record["duration"], "", 128, 0.0, 5 + ) item_record["position"] = RPR_GetMediaItemInfo_Value(this_item, "D_POSITION") - _, item_record["position_tc"], _, _ = RPR_format_timestr_pos(item_record["position"], "", 128, 5) + _, item_record["position_tc"], _, _ = RPR_format_timestr_pos( + item_record["position"], "", 128, 5 + ) - item_record["selected"] = True if RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0. else False - _, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String(this_item, "P_NOTES", "", False) - _, _, _, item_record["item_guid"], _ = RPR_GetSetMediaItemInfo_String(this_item, "GUID", "", False) + item_record["selected"] = RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0 + _, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String( + this_item, "P_NOTES", "", False + ) + _, _, _, item_record["item_guid"], _ = RPR_GetSetMediaItemInfo_String( + this_item, "GUID", "", False + ) active_take = RPR_GetActiveTake(this_item) - _, _, _, item_record["active_take_name"], _ = RPR_GetSetMediaItemTakeInfo_String(active_take, "P_NAME", "", False) - _, _, _, item_record["active_take_guid"], _ = RPR_GetSetMediaItemTakeInfo_String(active_take, "GUID", "", False) + _, _, _, item_record["active_take_name"], _ = RPR_GetSetMediaItemTakeInfo_String( + active_take, "P_NAME", "", False + ) + _, _, _, item_record["active_take_guid"], _ = RPR_GetSetMediaItemTakeInfo_String( + active_take, "GUID", "", False + ) item_track = RPR_GetMediaItemTrack(this_item) - _, _, _, item_record["track_name"], _ = RPR_GetSetMediaTrackInfo_String(item_track, "P_NAME", "", False) - _, _, _, item_record["track_guid"], _ = RPR_GetSetMediaTrackInfo_String(item_track, "GUID", "", False) - item_record["track_index"] = RPR_GetMediaTrackInfo_Value(item_track, "IP_TRACKNUMBER") - item_record["track_muted"] = True if RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0. else False + _, _, _, item_record["track_name"], _ = RPR_GetSetMediaTrackInfo_String( + item_track, "P_NAME", "", False + ) + _, _, _, item_record["track_guid"], _ = RPR_GetSetMediaTrackInfo_String( + item_track, "GUID", "", False + ) + item_record["track_index"] = RPR_GetMediaTrackInfo_Value( + item_track, "IP_TRACKNUMBER" + ) + item_record["track_muted"] = RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0.0 item_records = item_records + [item_record] -output = dict() +output = {} output["items"] = item_records _, output["project_title"], _ = RPR_GetProjectName(0, "", 1024) _, _, output["project_author"], _ = RPR_GetSetProjectAuthor(0, False, "", 1024) -output["project_frame_rate"], _, output["project_drop_frame"] = RPR_TimeMap_curFrameRate(0, True) +output["project_frame_rate"], _, output["project_drop_frame"] = ( + RPR_TimeMap_curFrameRate(0, True) +) output_path, _ = RPR_GetProjectPath("", 1024) @@ -51,12 +72,15 @@ output_title = output["project_title"] if output_title == "": output_title = "unsaved project" -output_file_name = "%s Text Export %s.txt" % (output_title, now.strftime('%Y%m%d_%H%M')) +output_file_name = f"{output_title} Text Export {now.strftime('%Y%m%d_%H%M')}.txt" output_path = output_path + "/" + output_file_name with open(output_path, "w") as f: json.dump(output, f, allow_nan=True, indent=4) -RPR_ShowMessageBox("Exported text file \"%s\" to project folder." % output_file_name, "Text Export Complete", 0) +RPR_ShowMessageBox( + f'Exported text file "{output_file_name}" to project folder. Text Export Complete', + 0, +) -#RPR_ShowConsoleMsg(output_path) \ No newline at end of file +# RPR_ShowConsoleMsg(output_path) diff --git a/src/ptulsconv/__main__.py b/src/ptulsconv/__main__.py index e2b450e..20da126 100644 --- a/src/ptulsconv/__main__.py +++ b/src/ptulsconv/__main__.py @@ -1,23 +1,24 @@ -from optparse import OptionParser, OptionGroup import datetime -import sys - import importlib.metadata +import sys +from optparse import OptionGroup, OptionParser -from ptulsconv import __name__ import ptulsconv +from ptulsconv import __name__ from ptulsconv.commands import convert -from ptulsconv.reporting import print_status_style, \ - print_banner_style, print_section_header_style, \ - print_fatal_error +from ptulsconv.docparser.adr_entity import ADR_TAG_MAPPING, GENERIC_TAG_MAPPING +from ptulsconv.reporting import ( + print_banner_style, + print_section_header_style, + print_status_style, +) def dump_field_map(output=sys.stdout): from ptulsconv.docparser.tag_mapping import TagMapping - from ptulsconv.docparser.adr_entity import ADRLine, GenericEvent - TagMapping.print_rules(GenericEvent, output=output) - TagMapping.print_rules(ADRLine, output=output) + TagMapping.print_rules(GENERIC_TAG_MAPPING, output=output) + TagMapping.print_rules(ADR_TAG_MAPPING, output=output) def dump_formats(): @@ -26,10 +27,12 @@ def dump_formats(): print_section_header_style("`tagged` Format:") sys.stderr.write( "A JSON document containing one record for each clip, with\n" - "all tags parsed and all tagging rules applied. \n") + "all tags parsed and all tagging rules applied. \n" + ) print_section_header_style("`doc` format:") - sys.stderr.write("Creates a directory with folders for different types\n" - "of ADR reports.\n\n") + sys.stderr.write( + "Creates a directory with folders for different types\nof ADR reports.\n\n" + ) def main(): @@ -37,54 +40,60 @@ def main(): parser = OptionParser() parser.usage = "ptulsconv [options] [TEXT_EXPORT.txt]" - parser.add_option('-f', '--format', - dest='output_format', - metavar='FMT', - choices=['raw', 'tagged', 'doc'], - default='doc', - help='Set output format, `raw`, `tagged`, `doc`.') + parser.add_option( + "-f", + "--format", + dest="output_format", + metavar="FMT", + choices=["raw", "tagged", "doc"], + default="doc", + help="Set output format, `raw`, `tagged`, `doc`.", + ) - parser.add_option('-m', '--movie-opts', - dest='movie_opts', - metavar="MOVIE_OPTS", - help="Set movie options") + parser.add_option( + "-m", + "--movie-opts", + dest="movie_opts", + metavar="MOVIE_OPTS", + help="Set movie options", + ) - warn_options = OptionGroup(title="Warning and Validation Options", - parser=parser) + warn_options = OptionGroup(title="Warning and Validation Options", parser=parser) - warn_options.add_option('-W', action='store_false', - dest='warnings', - default=True, - help='Suppress warnings for common ' - 'errors (missing code numbers etc.)') + warn_options.add_option( + "-W", + action="store_false", + dest="warnings", + default=True, + help="Suppress warnings for common errors (missing code numbers etc.)", + ) parser.add_option_group(warn_options) - informational_options = OptionGroup(title="Informational Options", - parser=parser, - description='Print useful ' - 'information ' - 'and exit without processing ' - 'input files.') + informational_options = OptionGroup( + title="Informational Options", + parser=parser, + description="Print useful information and exit without processing input files.", + ) informational_options.add_option( - '--show-formats', - dest='show_formats', - action='store_true', + "--show-formats", + dest="show_formats", + action="store_true", default=False, - help='Display helpful information about the available ' - 'output formats.') + help="Display helpful information about the available output formats.", + ) informational_options.add_option( - '--show-available-tags', - dest='show_tags', - action='store_true', + "--show-available-tags", + dest="show_tags", + action="store_true", default=False, - help='Display tag mappings for the FMP XML output style ' - 'and exit.') + help="Display tag mappings for the FMP XML output style and exit.", + ) parser.add_option_group(informational_options) - + version = importlib.metadata.version(ptulsconv.__name__) print_banner_style(f"{ptulsconv.__name__} - version {version}") print_banner_style(ptulsconv.__copyright__) @@ -92,8 +101,7 @@ def main(): (options, args) = parser.parse_args(sys.argv) print_section_header_style("Startup") - print_status_style("This run started %s" % - (datetime.datetime.now().isoformat())) + print_status_style(f"This run started {datetime.datetime.now().isoformat()}") if options.show_tags: dump_field_map() @@ -102,29 +110,16 @@ def main(): elif options.show_formats: dump_formats() sys.exit(0) - try: - major_mode = options.output_format - if len(args) < 2: - print_status_style( - "No input file provided, will connect to Pro Tools " - "with PTSL...") - convert(major_mode=major_mode, - warnings=options.warnings) - else: - convert(input_file=args[1], - major_mode=major_mode, - warnings=options.warnings) + major_mode = options.output_format - except FileNotFoundError as e: - print_fatal_error("Error trying to read input file") - raise e - - except Exception as e: - import traceback - print_fatal_error("Error trying to convert file") - print("\033[31m" + e.__repr__() + "\033[0m", file=sys.stderr) - print(traceback.format_exc()) + if len(args) < 2: + print_status_style( + "No input file provided, will connect to Pro Tools with PTSL..." + ) + convert(major_mode=major_mode, warnings=options.warnings) + else: + convert(input_file=args[1], major_mode=major_mode, warnings=options.warnings) if __name__ == "__main__": diff --git a/src/ptulsconv/broadcast_timecode.py b/src/ptulsconv/broadcast_timecode.py index 0c423eb..51d64d8 100644 --- a/src/ptulsconv/broadcast_timecode.py +++ b/src/ptulsconv/broadcast_timecode.py @@ -2,35 +2,40 @@ Useful functions for parsing and working with timecode. """ +from __future__ import annotations + import math import re -from collections import namedtuple from fractions import Fraction -from typing import Optional, SupportsFloat +from typing import NamedTuple -class TimecodeFormat(namedtuple("_TimecodeFormat", - "frame_duration logical_fps drop_frame")): +class TimecodeFormat(NamedTuple): """ A struct reperesenting a timecode datum. """ - def smpte_to_seconds(self, smpte: str) -> Optional[Fraction]: + frame_duration: Fraction + logical_fps: int + drop_frame: bool + + def smpte_to_seconds(self, smpte: str) -> Fraction | None: frame_count = smpte_to_frame_count( - smpte, self.logical_fps, drop_frame_hint=self.drop_frame) + smpte, self.logical_fps, drop_frame_hint=self.drop_frame + ) if frame_count is None: return None else: return frame_count * self.frame_duration - def seconds_to_smpte(self, seconds: SupportsFloat) -> str: + def seconds_to_smpte(self, seconds: Fraction) -> str: frame_count = int(seconds / self.frame_duration) - return frame_count_to_smpte(frame_count, self.logical_fps, - self.drop_frame) + return frame_count_to_smpte(frame_count, self.logical_fps, self.drop_frame) -def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, - drop_frame_hint=False) -> Optional[int]: +def smpte_to_frame_count( + smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False +) -> int | None: """ Convert a string with a SMPTE timecode representation into a frame count. @@ -44,15 +49,13 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, """ assert frames_per_logical_second in [24, 25, 30, 48, 50, 60] - m = re.search( - r'(\d?\d)[:;](\d\d)[:;](\d\d)([:;])(\d\d)(\.\d+)?', smpte_rep_string) + m = re.search(r"(\d?\d)[:;](\d\d)[:;](\d\d)([:;])(\d\d)(\.\d+)?", smpte_rep_string) if m is None: return None hh, mm, ss, sep, ff, frac = m.groups() - hh, mm, ss, ff, frac = int(hh), int( - mm), int(ss), int(ff), float(frac or 0.0) + hh, mm, ss, ff, frac = int(hh), int(mm), int(ss), int(ff), float(frac or 0.0) drop_frame = drop_frame_hint if sep == ";": @@ -61,12 +64,16 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, if frames_per_logical_second not in [30, 60]: drop_frame = False - raw_frames = hh * 3600 * frames_per_logical_second + mm * 60 * \ - frames_per_logical_second + ss * frames_per_logical_second + ff + raw_frames = ( + hh * 3600 * frames_per_logical_second + + mm * 60 * frames_per_logical_second + + ss * frames_per_logical_second + + ff + ) frames = raw_frames if drop_frame is True: - frames_dropped_per_inst = (frames_per_logical_second / 15) + frames_dropped_per_inst = frames_per_logical_second / 15 mins = hh * 60 + mm inst_count = mins - math.floor(mins / 10) dropped_frames = int(frames_dropped_per_inst) * inst_count @@ -75,9 +82,12 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, return frames -def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, - drop_frame: bool = False, - fractional_frame: Optional[float] = None) -> str: +def frame_count_to_smpte( + frame_count: int, + frames_per_logical_second: int, + drop_frame: bool = False, + fractional_frame: float | None = None, +) -> str: assert frames_per_logical_second in [24, 25, 30, 48, 50, 60] assert fractional_frame is None or fractional_frame < 1.0 @@ -86,7 +96,7 @@ def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, if drop_frame: assert frames_per_logical_second in [30, 60] mins, _ = divmod(nominal_frames, frames_per_logical_second * 60) - frames_dropped_per_inst = (frames_per_logical_second / 15) + frames_dropped_per_inst = frames_per_logical_second / 15 inst_count = mins - math.floor(mins / 10) dropped_frames = frames_dropped_per_inst * inst_count nominal_frames = nominal_frames + dropped_frames @@ -97,15 +107,22 @@ def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, ss, ff = divmod(rem, frames_per_logical_second) hh = hh % 24 + + hh = int(hh) + mm = int(mm) + ss = int(ss) + ff = int(ff) + if fractional_frame is not None and fractional_frame > 0: - return "%02i:%02i:%02i%s%02i%s" % (hh, mm, ss, separator, ff, - ("%.3f" % fractional_frame)[1:]) + fpart = "{fractional_frame:.3f}"[1:] + return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}{fpart}" else: - return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff) + return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}" + # "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff) -def footage_to_frame_count(footage_string) -> Optional[int]: - m = re.search(r'(\d+)\+(\d+)(\.\d+)?', footage_string) +def footage_to_frame_count(footage_string) -> int | None: + m = re.search(r"(\d+)\+(\d+)(\.\d+)?", footage_string) if m is None: return None feet, frm, frac = m.groups() @@ -118,4 +135,4 @@ def footage_to_frame_count(footage_string) -> Optional[int]: def frame_count_to_footage(frame_count): feet, frm = divmod(frame_count, 16) - return "%i+%02i" % (feet, frm) + return f"{feet}+{frm:02}" diff --git a/src/ptulsconv/commands.py b/src/ptulsconv/commands.py index 979f62d..dac98bf 100644 --- a/src/ptulsconv/commands.py +++ b/src/ptulsconv/commands.py @@ -2,34 +2,36 @@ This module provides the main input document parsing and transform implementation. """ + +from __future__ import annotations + +import csv import datetime import os - import sys -from itertools import chain -import csv -from typing import List, Optional, Iterator +from collections.abc import Iterator from fractions import Fraction +from itertools import chain +from json import JSONEncoder import ptsl -from .docparser.adr_entity import make_entities, ADRLine -from .reporting import print_section_header_style, print_status_style, \ - print_warning -from .validations import validate_unique_field, validate_non_empty_field, \ - validate_dependent_value - +from ptulsconv.broadcast_timecode import TimecodeFormat from ptulsconv.docparser import parse_document from ptulsconv.docparser.tag_compiler import TagCompiler -from ptulsconv.broadcast_timecode import TimecodeFormat - -from ptulsconv.pdf.supervisor_1pg import output_report as output_supervisor_1pg -from ptulsconv.pdf.line_count import output_report as output_line_count -from ptulsconv.pdf.talent_sides import output_report as output_talent_sides -from ptulsconv.pdf.summary_log import output_report as output_summary from ptulsconv.pdf.continuity import output_report as output_continuity +from ptulsconv.pdf.line_count import output_report as output_line_count +from ptulsconv.pdf.summary_log import output_report as output_summary +from ptulsconv.pdf.supervisor_1pg import output_report as output_supervisor_1pg +from ptulsconv.pdf.talent_sides import output_report as output_talent_sides -from json import JSONEncoder +from .docparser.adr_entity import ADRLine, make_entities +from .reporting import print_section_header_style, print_status_style, print_warning +from .validations import ( + validate_dependent_value, + validate_non_empty_field, + validate_unique_field, +) class FractionEncoder(JSONEncoder): @@ -37,97 +39,121 @@ class FractionEncoder(JSONEncoder): A subclass of :class:`JSONEncoder` which encodes :class:`Fraction` objects as a dict. """ - force_denominator: Optional[int] + + force_denominator: int | None def default(self, o): - """ - - """ if isinstance(o, Fraction): - return dict(numerator=o.numerator, denominator=o.denominator) + return {"numerator": o.numerator, "denominator": o.denominator} else: return o.__dict__ -def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat): +def output_adr_csv(lines: list[ADRLine], time_format: TimecodeFormat): """ Writes ADR lines as CSV to the current working directory. Creates directories for each character number and name pair, and within that directory, creates a CSV file for each reel. """ - reels: set[str | None] = set([ln.reel for ln in lines]) + reels: set[str | None] = {ln.reel for ln in lines} for n, name in [(n.character_id, n.character_name) for n in lines]: - dir_name = "%s_%s" % (n, name) + dir_name = f"{n}_{name}" os.makedirs(dir_name, exist_ok=True) os.chdir(dir_name) for reel in reels: - these_lines = [ln for ln in lines - if ln.character_id == n and ln.reel == reel] + these_lines = [ + ln for ln in lines if ln.character_id == n and ln.reel == reel + ] if len(these_lines) == 0: continue - outfile_name = "%s_%s_%s_%s.csv" % (these_lines[0].title, n, - these_lines[0].character_name, - reel,) + outfile_name = ( + f"{these_lines[0].title}_{n}_{these_lines[0].character_name}_{reel}.csv" + ) - with open(outfile_name, mode='w', newline='') as outfile: - writer = csv.writer(outfile, dialect='excel') - writer.writerow(['Title', 'Character Name', 'Cue Number', - 'Reel', 'Version', - 'Start', 'Finish', - 'Start Seconds', 'Finish Seconds', - 'Prompt', - 'Reason', 'Note', 'TV']) + with open(outfile_name, mode="w", newline="") as outfile: + writer = csv.writer(outfile, dialect="excel") + writer.writerow( + [ + "Title", + "Character Name", + "Cue Number", + "Reel", + "Version", + "Start", + "Finish", + "Start Seconds", + "Finish Seconds", + "Prompt", + "Reason", + "Note", + "TV", + ] + ) for event in these_lines: this_start = event.start or 0 this_finish = event.finish or 0 - this_row = [event.title, event.character_name, - event.cue_number, event.reel, event.version, - time_format.seconds_to_smpte(this_start), - time_format.seconds_to_smpte(this_finish), - float(this_start), float(this_finish), - event.prompt, - event.reason, event.note, "TV" - if event.tv else ""] + this_row = [ + event.title, + event.character_name, + event.cue_number, + event.reel, + event.version, + time_format.seconds_to_smpte(this_start), + time_format.seconds_to_smpte(this_finish), + float(this_start), + float(this_finish), + event.prompt, + event.reason, + event.note, + "TV" if event.tv else "", + ] writer.writerow(this_row) os.chdir("..") -def generate_documents(session_tc_format, scenes, adr_lines: List[ADRLine], - title): +def generate_documents(session_tc_format, scenes, adr_lines: list[ADRLine], title): """ Create PDF output. """ print_section_header_style("Creating PDF Reports") report_date = datetime.datetime.now() - reports_dir = "%s_%s" % (title, report_date.strftime("%Y-%m-%d_%H%M%S")) + reports_dir = f"{title}_{report_date.strftime('%Y-%m-%d_%H%M%S')}" os.makedirs(reports_dir, exist_ok=False) os.chdir(reports_dir) client = next((x.client for x in adr_lines), "") supervisor = next((x.supervisor for x in adr_lines), "") - output_continuity(scenes=scenes, tc_display_format=session_tc_format, - title=title, client=client or "", - supervisor=supervisor) + output_continuity( + scenes=scenes, + tc_display_format=session_tc_format, + title=title, + client=client or "", + supervisor=supervisor, + ) - reels = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6'] + reels = ["R1", "R2", "R3", "R4", "R5", "R6"] if len(adr_lines) == 0: - print_status_style("No ADR lines were found in the input document. " - "ADR reports will not be generated.") + print_status_style( + "No ADR lines were found in the input document. " + "ADR reports will not be generated." + ) else: - create_adr_reports(adr_lines, tc_display_format=session_tc_format, - reel_list=sorted(reels)) + create_adr_reports( + adr_lines, tc_display_format=session_tc_format, reel_list=sorted(reels) + ) -def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat, - reel_list: List[str]): +def create_adr_reports( + lines: list[ADRLine], tc_display_format: TimecodeFormat, reel_list: list[str] +): """ Creates a directory heirarchy and a respective set of ADR reports, given a list of lines. @@ -148,8 +174,7 @@ def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat, print_status_style("Creating Director's Logs director and reports") os.makedirs("Director Logs", exist_ok=True) os.chdir("Director Logs") - output_summary(lines, tc_display_format=tc_display_format, - by_character=True) + output_summary(lines, tc_display_format=tc_display_format, by_character=True) os.chdir("..") print_status_style("Creating CSV outputs") @@ -178,8 +203,8 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True): session_text = file.read() else: with ptsl.open_engine( - company_name="The ptulsconv developers", - application_name="ptulsconv") as engine: + company_name="The ptulsconv developers", application_name="ptulsconv" + ) as engine: req = engine.export_session_as_text() req.utf8_encoding() req.include_track_edls() @@ -192,7 +217,7 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True): session = parse_document(session_text) session_tc_format = session.header.timecode_format - if major_mode == 'raw': + if major_mode == "raw": output.write(FractionEncoder().encode(session)) else: @@ -200,35 +225,35 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True): compiler.session = session compiled_events = list(compiler.compile_events()) - if major_mode == 'tagged': + if major_mode == "tagged": output.write(FractionEncoder().encode(compiled_events)) - elif major_mode == 'doc': + elif major_mode == "doc": generic_events, adr_lines = make_entities(compiled_events) - scenes = sorted([s for s in compiler.compile_all_time_spans() - if s[0] == 'Sc'], - key=lambda x: x[2]) + scenes = sorted( + [s for s in compiler.compile_all_time_spans() if s[0] == "Sc"], + key=lambda x: x[2], + ) # TODO: Breakdown by titles - titles = set([x.title for x in (generic_events + adr_lines)]) + titles = {x.title for x in (generic_events + adr_lines)} if len(titles) != 1: - print_warning("Multiple titles per export is not supported, " - "found multiple titles: %s Exiting." % titles) - exit(-1) + print_warning( + "Multiple titles per export is not supported, " + f"found multiple titles: {titles} Exiting." + ) + sys.exit(-1) - title = list(titles)[0] + title = next(iter(titles)) - print_status_style( - "%i generic events found." % len(generic_events) - ) - print_status_style("%i ADR events found." % len(adr_lines)) + print_status_style(f"{len(generic_events)} generic events found.") + print_status_style(f"{len(adr_lines)} ADR events found.") if warnings: perform_adr_validations(iter(adr_lines)) - generate_documents(session_tc_format, scenes, adr_lines, - title) + generate_documents(session_tc_format, scenes, adr_lines, title) def perform_adr_validations(lines: Iterator[ADRLine]): @@ -236,20 +261,15 @@ def perform_adr_validations(lines: Iterator[ADRLine]): Performs validations on the input. """ for warning in chain( - validate_unique_field(lines, - field='cue_number', - scope='title'), - validate_non_empty_field(lines, - field='cue_number'), - validate_non_empty_field(lines, - field='character_id'), - validate_non_empty_field(lines, - field='title'), - validate_dependent_value(lines, - key_field='character_id', - dependent_field='character_name'), - validate_dependent_value(lines, - key_field='character_id', - dependent_field='actor_name')): - + validate_unique_field(lines, field="cue_number", scope="title"), + validate_non_empty_field(lines, field="cue_number"), + validate_non_empty_field(lines, field="character_id"), + validate_non_empty_field(lines, field="title"), + validate_dependent_value( + lines, key_field="character_id", dependent_field="character_name" + ), + validate_dependent_value( + lines, key_field="character_id", dependent_field="actor_name" + ), + ): print_warning(warning.report_message()) diff --git a/src/ptulsconv/docparser/__init__.py b/src/ptulsconv/docparser/__init__.py index 840e568..b4d2a01 100644 --- a/src/ptulsconv/docparser/__init__.py +++ b/src/ptulsconv/docparser/__init__.py @@ -2,7 +2,4 @@ Docparser module """ - -from .pt_doc_parser import parse_document - -__all__ = [parse_document] +from .pt_doc_parser import parse_document # pyright: ignore[reportUnusedImport, reportUnusedImport] # noqa: F401, I001 diff --git a/src/ptulsconv/docparser/adr_entity.py b/src/ptulsconv/docparser/adr_entity.py index 597bc64..1307125 100644 --- a/src/ptulsconv/docparser/adr_entity.py +++ b/src/ptulsconv/docparser/adr_entity.py @@ -3,16 +3,53 @@ This module defines classes and methods for converting :class:`Event` objects into :class:`ADRLine` objects. """ -from ptulsconv.docparser.tag_compiler import Event -from typing import Optional, List, Tuple +from __future__ import annotations + from dataclasses import dataclass from fractions import Fraction +from typing import ClassVar +from ptulsconv.docparser.tag_compiler import Event from ptulsconv.docparser.tag_mapping import TagMapping +GENERIC_TAG_MAPPING = [ + TagMapping(source="Title", target="title", alt=TagMapping.ContentSource.Session), + TagMapping(source="Supv", target="supervisor"), + TagMapping(source="Client", target="client"), + TagMapping(source="Sc", target="scene"), + TagMapping(source="Ver", target="version"), + TagMapping(source="Reel", target="reel"), + TagMapping(source="Note", target="note"), + TagMapping(source="Rq", target="requested_by"), + TagMapping(source="OMIT", target="omitted", formatter=(lambda x: len(x) > 0)), +] -def make_entities(from_events: List[Event]) -> Tuple[List['GenericEvent'], - List['ADRLine']]: +ADR_TAG_MAPPING = [ + TagMapping(source="P", target="priority"), + TagMapping(source="QN", target="cue_number"), + TagMapping(source="CN", target="character_id"), + TagMapping( + source="Char", target="character_name", alt=TagMapping.ContentSource.Track + ), + TagMapping(source="Actor", target="actor_name"), + TagMapping(source="Line", target="prompt", alt=TagMapping.ContentSource.Clip), + TagMapping(source="R", target="reason"), + TagMapping( + source="Mins", target="time_budget_mins", formatter=(lambda n: float(n)) + ), + TagMapping(source="Spot", target="spot"), + TagMapping(source="Shot", target="shot"), + TagMapping(source="EFF", target="effort", formatter=(lambda x: len(x) > 0)), + TagMapping(source="TV", target="tv", formatter=(lambda x: len(x) > 0)), + TagMapping(source="TBW", target="tbw", formatter=(lambda x: len(x) > 0)), + TagMapping(source="ADLIB", target="adlib", formatter=(lambda x: len(x) > 0)), + TagMapping(source="OPT", target="optional", formatter=(lambda x: len(x) > 0)), +] + + +def make_entities( + from_events: list[Event], +) -> tuple[list[GenericEvent], list[ADRLine]]: """ Accepts a list of Events and converts them into either ADRLine events or GenricEvents by calling :func:`make_entity` on each member. @@ -22,8 +59,8 @@ def make_entities(from_events: List[Event]) -> Tuple[List['GenericEvent'], :returns: A tuple of two lists, the first containing :class:`GenericEvent` and the second containing :class:`ADRLine`. """ - generic_events = list() - adr_lines = list() + generic_events = [] + adr_lines = [] for event in from_events: result = make_entity(event) @@ -35,7 +72,7 @@ def make_entities(from_events: List[Event]) -> Tuple[List['GenericEvent'], return generic_events, adr_lines -def make_entity(from_event: Event) -> Optional[object]: +def make_entity(from_event: Event) -> object | None: """ Accepts an event and creates either an :class:`ADRLine` or a :class:`GenericEvent`. An event is an "ADRLine" if it has a cue number/"QN" @@ -45,15 +82,20 @@ def make_entity(from_event: Event) -> Optional[object]: """ instance = GenericEvent - tag_map = GenericEvent.tag_mapping - if 'QN' in from_event.tags.keys(): + tag_map = GENERIC_TAG_MAPPING + if "QN" in from_event.tags: instance = ADRLine - tag_map += ADRLine.tag_mapping + tag_map += ADR_TAG_MAPPING new = instance() - TagMapping.apply_rules(tag_map, from_event.tags, - from_event.clip_name, from_event.track_name, - from_event.session_name, new) + TagMapping.apply_rules( + rules=tag_map, + tags=from_event.tags, + clip_content=from_event.clip_name, + track_content=from_event.track_name, + session_content=from_event.session_name, + to=new, + ) new.start = from_event.start new.finish = from_event.finish @@ -63,75 +105,36 @@ def make_entity(from_event: Event) -> Optional[object]: @dataclass class GenericEvent: title: str = "" - supervisor: Optional[str] = None - client: Optional[str] = None - scene: Optional[str] = None - version: Optional[str] = None - reel: Optional[str] = None + supervisor: str | None = None + client: str | None = None + scene: str | None = None + version: str | None = None + reel: str | None = None start: Fraction = Fraction(0, 1) finish: Fraction = Fraction(0, 1) omitted: bool = False - note: Optional[str] = None - requested_by: Optional[str] = None + note: str | None = None + requested_by: str | None = None - tag_mapping = [ - TagMapping(source='Title', target="title", - alt=TagMapping.ContentSource.Session), - TagMapping(source="Supv", target="supervisor"), - TagMapping(source="Client", target="client"), - TagMapping(source="Sc", target="scene"), - TagMapping(source="Ver", target="version"), - TagMapping(source="Reel", target="reel"), - TagMapping(source="Note", target="note"), - TagMapping(source="Rq", target="requested_by"), - TagMapping(source="OMIT", target="omitted", - formatter=(lambda x: len(x) > 0)), - ] + tag_mapping: ClassVar = GENERIC_TAG_MAPPING @dataclass class ADRLine(GenericEvent): - - priority: Optional[int] = None - cue_number: Optional[str] = None - character_id: Optional[str] = None - character_name: Optional[str] = None - actor_name: Optional[str] = None - prompt: Optional[str] = None - reason: Optional[str] = None - time_budget_mins: Optional[float] = None - spot: Optional[str] = None - shot: Optional[str] = None + priority: int | None = None + cue_number: str | None = None + character_id: str | None = None + character_name: str | None = None + actor_name: str | None = None + prompt: str | None = None + reason: str | None = None + time_budget_mins: float | None = None + spot: str | None = None + shot: str | None = None effort: bool = False tv: bool = False tbw: bool = False adlib: bool = False optional: bool = False - tag_mapping = [ - - TagMapping(source="P", target="priority"), - TagMapping(source="QN", target="cue_number"), - TagMapping(source="CN", target="character_id"), - TagMapping(source="Char", target="character_name", - alt=TagMapping.ContentSource.Track), - TagMapping(source="Actor", target="actor_name"), - TagMapping(source="Line", target="prompt", - alt=TagMapping.ContentSource.Clip), - TagMapping(source="R", target="reason"), - TagMapping(source="Mins", target="time_budget_mins", - formatter=(lambda n: float(n))), - TagMapping(source="Spot", target="spot"), - TagMapping(source="Shot", target="shot"), - TagMapping(source="EFF", target="effort", - formatter=(lambda x: len(x) > 0)), - TagMapping(source="TV", target="tv", - formatter=(lambda x: len(x) > 0)), - TagMapping(source="TBW", target="tbw", - formatter=(lambda x: len(x) > 0)), - - TagMapping(source="ADLIB", target="adlib", - formatter=(lambda x: len(x) > 0)), - TagMapping(source="OPT", target="optional", - formatter=(lambda x: len(x) > 0)) - ] + tag_mapping: ClassVar = ADR_TAG_MAPPING diff --git a/src/ptulsconv/docparser/doc_entity.py b/src/ptulsconv/docparser/doc_entity.py index f20afd8..bc0b30f 100644 --- a/src/ptulsconv/docparser/doc_entity.py +++ b/src/ptulsconv/docparser/doc_entity.py @@ -1,42 +1,42 @@ +from __future__ import annotations + +from collections.abc import Iterator from fractions import Fraction + from ptulsconv.broadcast_timecode import TimecodeFormat -from typing import Tuple, List, Iterator class SessionDescriptor: - header: "HeaderDescriptor" - files: List["FileDescriptor"] - clips: List["ClipDescriptor"] - plugins: List["PluginDescriptor"] - tracks: List["TrackDescriptor"] - markers: List["MarkerDescriptor"] + header: HeaderDescriptor + files: list[FileDescriptor] + clips: list[ClipDescriptor] + plugins: list[PluginDescriptor] + tracks: list[TrackDescriptor] + markers: list[MarkerDescriptor] def __init__(self, **kwargs): - self.header = kwargs['header'] - self.files = kwargs['files'] - self.clips = kwargs['clips'] - self.plugins = kwargs['plugins'] - self.tracks = kwargs['tracks'] - self.markers = kwargs['markers'] + self.header = kwargs["header"] + self.files = kwargs["files"] + self.clips = kwargs["clips"] + self.plugins = kwargs["plugins"] + self.tracks = kwargs["tracks"] + self.markers = kwargs["markers"] - def markers_timed(self, - only_ruler_markers: bool = True) -> \ - Iterator[Tuple['MarkerDescriptor', Fraction]]: + def markers_timed( + self, only_ruler_markers: bool = True + ) -> Iterator[tuple[MarkerDescriptor, Fraction]]: """ Iterate each marker in the session with its respective time reference. """ for marker in self.markers: - if marker.track_marker and only_ruler_markers: continue - marker_time = Fraction(marker.time_reference, - int(self.header.sample_rate)) + marker_time = Fraction(marker.time_reference, int(self.header.sample_rate)) # marker_time = self.header.convert_timecode(marker.location) yield marker, marker_time - def tracks_clips(self) -> Iterator[Tuple['TrackDescriptor', - 'TrackClipDescriptor']]: + def tracks_clips(self) -> Iterator[tuple[TrackDescriptor, TrackClipDescriptor]]: """ Iterate each track clip with its respective owning clip. """ @@ -44,10 +44,11 @@ class SessionDescriptor: for clip in track.clips: yield track, clip - def track_clips_timed(self) -> Iterator[Tuple["TrackDescriptor", - "TrackClipDescriptor", - Fraction, Fraction, Fraction] - ]: + def track_clips_timed( + self, + ) -> Iterator[ + tuple[TrackDescriptor, TrackClipDescriptor, Fraction, Fraction, Fraction | None] + ]: """ Iterate each track clip with its respective owning clip and timing information. @@ -58,8 +59,11 @@ class SessionDescriptor: for track, clip in self.tracks_clips(): start_time = self.header.convert_timecode(clip.start_timecode) finish_time = self.header.convert_timecode(clip.finish_timecode) - timestamp_time = self.header.convert_timecode(clip.timestamp) \ - if clip.timestamp is not None else None + timestamp_time = ( + self.header.convert_timecode(clip.timestamp) + if clip.timestamp is not None + else None + ) yield track, clip, start_time, finish_time, timestamp_time @@ -76,27 +80,29 @@ class HeaderDescriptor: count_files: int def __init__(self, **kwargs): - self.session_name = kwargs['session_name'] - self.sample_rate = kwargs['sample_rate'] - self.bit_depth = kwargs['bit_depth'] - self.start_timecode = kwargs['start_timecode'] - self.timecode_fps = kwargs['timecode_format'] - self.timecode_drop_frame = kwargs['timecode_drop_frame'] - self.count_audio_tracks = kwargs['count_audio_tracks'] - self.count_clips = kwargs['count_clips'] - self.count_files = kwargs['count_files'] + self.session_name = kwargs["session_name"] + self.sample_rate = kwargs["sample_rate"] + self.bit_depth = kwargs["bit_depth"] + self.start_timecode = kwargs["start_timecode"] + self.timecode_fps = kwargs["timecode_format"] + self.timecode_drop_frame = kwargs["timecode_drop_frame"] + self.count_audio_tracks = kwargs["count_audio_tracks"] + self.count_clips = kwargs["count_clips"] + self.count_files = kwargs["count_files"] @property def timecode_format(self): - return TimecodeFormat(frame_duration=self.frame_duration, - logical_fps=self.logical_fps, - drop_frame=self.timecode_drop_frame) + return TimecodeFormat( + frame_duration=self.frame_duration, + logical_fps=self.logical_fps, + drop_frame=self.timecode_drop_frame, + ) - def convert_timecode(self, tc_string: str) -> Fraction: + def convert_timecode(self, tc_string: str) -> Fraction | None: return self.timecode_format.smpte_to_seconds(tc_string) @property - def start_time(self) -> Fraction: + def start_time(self) -> Fraction | None: """ The start time of this session. :return: Start time in seconds @@ -112,21 +118,21 @@ class HeaderDescriptor: return self._get_tc_format_params[1] @property - def _get_tc_format_params(self) -> Tuple[int, Fraction]: - frame_rates = {"23.976": (24, Fraction(1001, 24_000)), - "24": (24, Fraction(1, 24)), - "25": (25, Fraction(1, 25)), - "29.97": (30, Fraction(1001, 30_000)), - "30": (30, Fraction(1, 30)), - "59.94": (60, Fraction(1001, 60_000)), - "60": (60, Fraction(1, 60)) - } + def _get_tc_format_params(self) -> tuple[int, Fraction]: + frame_rates = { + "23.976": (24, Fraction(1001, 24_000)), + "24": (24, Fraction(1, 24)), + "25": (25, Fraction(1, 25)), + "29.97": (30, Fraction(1001, 30_000)), + "30": (30, Fraction(1, 30)), + "59.94": (60, Fraction(1001, 60_000)), + "60": (60, Fraction(1, 60)), + } - if self.timecode_fps in frame_rates.keys(): + if self.timecode_fps in frame_rates: return frame_rates[self.timecode_fps] else: - raise ValueError("Unrecognized TC rate (%s)" % - self.timecode_format) + raise ValueError(f"Unrecognized TC rate ({self.timecode_format})") class TrackDescriptor: @@ -134,18 +140,18 @@ class TrackDescriptor: name: str comments: str user_delay_samples: int - state: List[str] - plugins: List[str] - clips: List["TrackClipDescriptor"] + state: list[str] + plugins: list[str] + clips: list[TrackClipDescriptor] def __init__(self, **kwargs): - self.index = kwargs['index'] - self.name = kwargs['name'] - self.comments = kwargs['comments'] - self.user_delay_samples = kwargs['user_delay_samples'] - self.state = kwargs['state'] - self.plugins = kwargs['plugins'] - self.clips = kwargs['clips'] + self.index = kwargs["index"] + self.name = kwargs["name"] + self.comments = kwargs["comments"] + self.user_delay_samples = kwargs["user_delay_samples"] + self.state = kwargs["state"] + self.plugins = kwargs["plugins"] + self.clips = kwargs["clips"] class FileDescriptor(dict): @@ -163,14 +169,14 @@ class TrackClipDescriptor: state: str def __init__(self, **kwargs): - self.channel = kwargs['channel'] - self.event = kwargs['event'] - self.clip_name = kwargs['clip_name'] - self.start_timecode = kwargs['start_time'] - self.finish_timecode = kwargs['finish_time'] - self.duration = kwargs['duration'] - self.timestamp = kwargs['timestamp'] - self.state = kwargs['state'] + self.channel = kwargs["channel"] + self.event = kwargs["event"] + self.clip_name = kwargs["clip_name"] + self.start_timecode = kwargs["start_time"] + self.finish_timecode = kwargs["finish_time"] + self.duration = kwargs["duration"] + self.timestamp = kwargs["timestamp"] + self.state = kwargs["state"] class ClipDescriptor(dict): @@ -191,10 +197,10 @@ class MarkerDescriptor: track_marker: bool def __init__(self, **kwargs): - self.number = kwargs['number'] - self.location = kwargs['location'] - self.time_reference = kwargs['time_reference'] - self.units = kwargs['units'] - self.name = kwargs['name'] - self.comments = kwargs['comments'] - self.track_marker = kwargs['track_marker'] + self.number = kwargs["number"] + self.location = kwargs["location"] + self.time_reference = kwargs["time_reference"] + self.units = kwargs["units"] + self.name = kwargs["name"] + self.comments = kwargs["comments"] + self.track_marker = kwargs["track_marker"] diff --git a/src/ptulsconv/docparser/pt_doc_parser.py b/src/ptulsconv/docparser/pt_doc_parser.py index d54a359..28c1587 100644 --- a/src/ptulsconv/docparser/pt_doc_parser.py +++ b/src/ptulsconv/docparser/pt_doc_parser.py @@ -1,10 +1,16 @@ -from parsimonious.nodes import NodeVisitor from parsimonious.grammar import Grammar +from parsimonious.nodes import NodeVisitor -from .doc_entity import SessionDescriptor, HeaderDescriptor, TrackDescriptor, \ - FileDescriptor, TrackClipDescriptor, ClipDescriptor, PluginDescriptor, \ - MarkerDescriptor - +from .doc_entity import ( + ClipDescriptor, + FileDescriptor, + HeaderDescriptor, + MarkerDescriptor, + PluginDescriptor, + SessionDescriptor, + TrackClipDescriptor, + TrackDescriptor, +) protools_text_export_grammar = Grammar( r""" @@ -110,7 +116,8 @@ protools_text_export_grammar = Grammar( integer_value = ~r"\d+" float_value = ~r"\d+(\.\d+)?" isp = ~r"[^\d\t\n]*" - """) + """ +) def parse_document(session_text: str) -> SessionDescriptor: @@ -124,7 +131,6 @@ def parse_document(session_text: str) -> SessionDescriptor: class DocParserVisitor(NodeVisitor): - def __init__(self): self.track_index = 0 @@ -137,12 +143,14 @@ class DocParserVisitor(NodeVisitor): tracks = next(iter(visited_children[4]), None) markers = next(iter(visited_children[5]), None) - return SessionDescriptor(header=visited_children[0], - files=files, - clips=clips, - plugins=plugins, - tracks=tracks, - markers=markers) + return SessionDescriptor( + header=visited_children[0], + files=files, + clips=clips, + plugins=plugins, + tracks=tracks, + markers=markers, + ) @staticmethod def visit_header(_, visited_children): @@ -151,41 +159,47 @@ class DocParserVisitor(NodeVisitor): for _ in visited_children[20]: tc_drop = True - return HeaderDescriptor(session_name=visited_children[2], - sample_rate=visited_children[6], - bit_depth=visited_children[10], - start_timecode=visited_children[15], - timecode_format=visited_children[19], - timecode_drop_frame=tc_drop, - count_audio_tracks=visited_children[25], - count_clips=visited_children[29], - count_files=visited_children[33]) + return HeaderDescriptor( + session_name=visited_children[2], + sample_rate=visited_children[6], + bit_depth=visited_children[10], + start_timecode=visited_children[15], + timecode_format=visited_children[19], + timecode_drop_frame=tc_drop, + count_audio_tracks=visited_children[25], + count_clips=visited_children[29], + count_files=visited_children[33], + ) @staticmethod def visit_files_section(_, visited_children): - return list(map( - lambda child: FileDescriptor(filename=child[0], path=child[2]), - visited_children[2])) + return [ + FileDescriptor(filemname=child[0], path=child[2]) + for child in visited_children[2] + ] @staticmethod def visit_clips_section(_, visited_children): channel = next(iter(visited_children[2][3]), 1) - - return list(map( - lambda child: ClipDescriptor(clip_name=child[0], file=child[2], - channel=channel), - visited_children[2])) + return [ + ClipDescriptor(clip_name=child[0], file=child[2], channel=channel) + for child in visited_children[2] + ] @staticmethod def visit_plugin_listing(_, visited_children): - return list(map(lambda child: - PluginDescriptor(manufacturer=child[0], - plugin_name=child[2], - version=child[4], - format=child[6], - stems=child[8], - count_instances=child[10]), - visited_children[2])) + + return [ + PluginDescriptor( + manufacturer=child[0], + plugin_name=child[2], + version=child[4], + format=child[6], + stems=child[8], + count_instances=child[10], + ) + for child in visited_children[2] + ] # @staticmethod def visit_track_block(self, _, visited_children): @@ -210,7 +224,7 @@ class DocParserVisitor(NodeVisitor): user_delay_samples=track_header[10], state=track_header[14], plugins=plugins, - clips=clips + clips=clips, ) @staticmethod @@ -227,14 +241,16 @@ class DocParserVisitor(NodeVisitor): if isinstance(visited_children[14], list): timestamp = visited_children[14][0][0] - return TrackClipDescriptor(channel=visited_children[0], - event=visited_children[3], - clip_name=visited_children[6], - start_time=visited_children[8], - finish_time=visited_children[10], - duration=visited_children[12], - timestamp=timestamp, - state=visited_children[15]) + return TrackClipDescriptor( + channel=visited_children[0], + event=visited_children[3], + clip_name=visited_children[6], + start_time=visited_children[8], + finish_time=visited_children[10], + duration=visited_children[12], + timestamp=timestamp, + state=visited_children[15], + ) @staticmethod def visit_track_state_list(_, visited_children): @@ -252,6 +268,7 @@ class DocParserVisitor(NodeVisitor): markers = [] for marker in visited_children[1][0][1]: + marker: MarkerDescriptor markers.append(marker) return markers @@ -259,26 +276,30 @@ class DocParserVisitor(NodeVisitor): @staticmethod def visit_marker_record_simple(_, visited_children): - return MarkerDescriptor(number=visited_children[0], - location=visited_children[3], - time_reference=visited_children[5], - units=visited_children[8], - name=visited_children[10], - comments=visited_children[12], - track_marker=False) + return MarkerDescriptor( + number=visited_children[0], + location=visited_children[3], + time_reference=visited_children[5], + units=visited_children[8], + name=visited_children[10], + comments=visited_children[12], + track_marker=False, + ) @staticmethod def visit_marker_record(_, visited_children): track_type = visited_children[15] - is_track_marker = (track_type == "Track") + is_track_marker = track_type == "Track" - return MarkerDescriptor(number=visited_children[0], - location=visited_children[3], - time_reference=visited_children[5], - units=visited_children[8], - name=visited_children[10], - comments=visited_children[16], - track_marker=is_track_marker) + return MarkerDescriptor( + number=visited_children[0], + location=visited_children[3], + time_reference=visited_children[5], + units=visited_children[8], + name=visited_children[10], + comments=visited_children[16], + track_marker=is_track_marker, + ) @staticmethod def visit_formatted_clip_name(_, visited_children): @@ -303,5 +324,5 @@ class DocParserVisitor(NodeVisitor): pass def generic_visit(self, node, visited_children): - """ The generic visit method. """ + """The generic visit method.""" return visited_children or node diff --git a/src/ptulsconv/docparser/tag_compiler.py b/src/ptulsconv/docparser/tag_compiler.py index 1d9beba..fdf0b43 100644 --- a/src/ptulsconv/docparser/tag_compiler.py +++ b/src/ptulsconv/docparser/tag_compiler.py @@ -1,11 +1,13 @@ -from collections import namedtuple -from fractions import Fraction -from typing import Iterator, Tuple, Callable, Generator, Dict, List - -import ptulsconv.docparser.doc_entity as doc_entity -from .tagged_string_parser_visitor import parse_tags, TagPreModes +from __future__ import annotations +from collections.abc import Generator, Iterator from dataclasses import dataclass +from fractions import Fraction +from typing import Callable, NamedTuple + +from ptulsconv.docparser import doc_entity + +from .tagged_string_parser_visitor import TagPreModes, parse_tags @dataclass @@ -13,7 +15,7 @@ class Event: clip_name: str track_name: str session_name: str - tags: Dict[str, str] + tags: dict[str, str] start: Fraction finish: Fraction @@ -24,36 +26,41 @@ class TagCompiler: items. """ - Intermediate = namedtuple('Intermediate', - 'track_content track_tags track_comment_tags ' - 'clip_content clip_tags clip_tag_mode start ' - 'finish') + class Intermediate(NamedTuple): + track_content: str + clip_content: str + track_tags: dict[str, str] + track_comment_tags: dict[str, str] + clip_tags: dict[str, str] + clip_tag_mode: TagPreModes + start: Fraction + finish: Fraction session: doc_entity.SessionDescriptor - def compile_all_time_spans(self) -> List[Tuple[str, str, Fraction, - Fraction]]: + def compile_all_time_spans(self) -> list[tuple[str, str, Fraction, Fraction]]: """ :returns: A `List` of (key: str, value: str, start: Fraction, finish: Fraction) """ - ret_list = list() + ret_list = [] for element in self.parse_data(): if element.clip_tag_mode == TagPreModes.TIMESPAN: - for k in element.clip_tags.keys(): - ret_list.append((k, element.clip_tags[k], element.start, - element.finish)) + for k in element.clip_tags: + ret_list.append( + (k, element.clip_tags[k], element.start, element.finish) + ) return ret_list - def compile_tag_list(self) -> Dict[str, List[str]]: - tags_dict = dict() + def compile_tag_list(self) -> dict[str, list[str]]: + tags_dict = {} def update_tags_dict(other_dict: dict): - for k in other_dict.keys(): - if k not in tags_dict.keys(): + for k, value in other_dict.items(): + if k not in tags_dict: tags_dict[k] = set() - tags_dict[k].add(other_dict[k]) + tags_dict[k].add(value) for parsed in self.parse_data(): update_tags_dict(parsed.clip_tags) @@ -78,15 +85,19 @@ class TagCompiler: step3 = self.collect_time_spans(step2) step4 = self.apply_tags(step3) for datum in step4: - yield Event(clip_name=datum[0], track_name=datum[1], - session_name=datum[2], tags=datum[3], start=datum[4], - finish=datum[5]) + yield Event( + clip_name=datum[0], + track_name=datum[1], + session_name=datum[2], + tags=datum[3], + start=datum[4], + finish=datum[5], + ) def _marker_tags(self, at): - retval = dict() + retval = {} - applicable = [(m, t) for (m, t) in - self.session.markers_timed() if t <= at] + applicable = [(m, t) for (m, t) in self.session.markers_timed() if t <= at] for marker, _ in sorted(applicable, key=lambda x: x[1]): retval.update(parse_tags(marker.comments or "").tag_dict) @@ -94,21 +105,25 @@ class TagCompiler: return retval - def filter_out_directives(self, - clips: Iterator[Intermediate]) \ - -> Iterator[Intermediate]: + def filter_out_directives( + self, clips: Iterator[Intermediate] + ) -> Iterator[Intermediate]: for clip in clips: - if clip.clip_tag_mode == 'Directive': + if clip.clip_tag_mode == "Directive": continue else: yield clip @staticmethod - def _coalesce_tags(clip_tags: dict, track_tags: dict, - track_comment_tags: dict, - timespan_tags: dict, - marker_tags: dict, session_tags: dict): - effective_tags = dict() + def _coalesce_tags( + clip_tags: dict, + track_tags: dict, + track_comment_tags: dict, + timespan_tags: dict, + marker_tags: dict, + session_tags: dict, + ): + effective_tags = {} effective_tags.update(session_tags) effective_tags.update(marker_tags) effective_tags.update(timespan_tags) @@ -118,9 +133,8 @@ class TagCompiler: return effective_tags def parse_data(self) -> Iterator[Intermediate]: - for track, clip, start, finish, _ in self.session.track_clips_timed(): - if clip.state == 'Muted': + if clip.state == "Muted": continue track_parsed = parse_tags(track.name) @@ -134,15 +148,14 @@ class TagCompiler: clip_content=clip_parsed.content, clip_tags=clip_parsed.tag_dict, clip_tag_mode=clip_parsed.mode, - start=start, finish=finish) + start=start, + finish=finish, + ) @staticmethod - def apply_appends(parsed: Iterator[Intermediate]) -> \ - Iterator[Intermediate]: - + def apply_appends(parsed: Iterator[Intermediate]) -> Iterator[Intermediate]: def should_append(a, b): - return b.clip_tag_mode == TagPreModes.APPEND and \ - b.start >= a.finish + return b.clip_tag_mode == TagPreModes.APPEND and b.start >= a.finish def do_append(a, b): merged_tags = dict(a.clip_tags) @@ -151,62 +164,73 @@ class TagCompiler: track_content=a.track_content, track_tags=a.track_tags, track_comment_tags=a.track_comment_tags, - clip_content=a.clip_content + ' ' + b.clip_content, - clip_tags=merged_tags, clip_tag_mode=a.clip_tag_mode, - start=a.start, finish=b.finish) + clip_content=a.clip_content + " " + b.clip_content, + clip_tags=merged_tags, + clip_tag_mode=a.clip_tag_mode, + start=a.start, + finish=b.finish, + ) yield from apply_appends(parsed, should_append, do_append) @staticmethod - def collect_time_spans(parsed: Iterator[Intermediate]) -> \ - Iterator[Tuple[Intermediate, Tuple[dict, Fraction, Fraction]]]: - - time_spans = list() + def collect_time_spans( + parsed: Iterator[Intermediate], + ) -> Iterator[tuple[Intermediate, tuple[dict, Fraction, Fraction]]]: + time_spans = [] for item in parsed: if item.clip_tag_mode == TagPreModes.TIMESPAN: time_spans.append((item.clip_tags, item.start, item.finish)) else: - yield item, list(time_spans) + yield item, tuple(time_spans) @staticmethod def _time_span_tags(at_time: Fraction, applicable_spans) -> dict: - retval = dict() - for tags in reversed([a[0] for a in applicable_spans - if a[1] <= at_time <= a[2]]): + retval = {} + for tags in reversed( + [a[0] for a in applicable_spans if a[1] <= at_time <= a[2]] + ): retval.update(tags) return retval - def apply_tags(self, parsed_with_time_spans) ->\ - Iterator[Tuple[str, str, str, dict, Fraction, Fraction]]: - + def apply_tags( + self, parsed_with_time_spans + ) -> Iterator[tuple[str, str, str, dict, Fraction, Fraction]]: session_parsed = parse_tags(self.session.header.session_name) for event, time_spans in parsed_with_time_spans: - event: 'TagCompiler.Intermediate' + event: TagCompiler.Intermediate marker_tags = self._marker_tags(event.start) time_span_tags = self._time_span_tags(event.start, time_spans) tags = self._coalesce_tags( - clip_tags=event.clip_tags, - track_tags=event.track_tags, - track_comment_tags=event.track_comment_tags, - timespan_tags=time_span_tags, - marker_tags=marker_tags, - session_tags=session_parsed.tag_dict) + clip_tags=event.clip_tags, + track_tags=event.track_tags, + track_comment_tags=event.track_comment_tags, + timespan_tags=time_span_tags, + marker_tags=marker_tags, + session_tags=session_parsed.tag_dict, + ) - yield (event.clip_content, event.track_content, - session_parsed.content, tags, event.start, event.finish) + yield ( + event.clip_content, + event.track_content, + session_parsed.content, + tags, + event.start, + event.finish, + ) -def apply_appends(source: Iterator, - should_append: Callable, - do_append: Callable) -> Generator: +def apply_appends( + source: Iterator, should_append: Callable, preform_append: Callable +) -> Generator: """ :param source: :param should_append: Called with two variables a and b, your - function should return true if b should be - appended to a + function should return true if b should be + appended to a :param do_append: Called with two variables a and b, your function should return :returns: A Generator @@ -214,7 +238,7 @@ def apply_appends(source: Iterator, this_element = next(source) for element in source: if should_append(this_element, element): - this_element = do_append(this_element, element) + this_element = preform_append(this_element, element) else: yield this_element this_element = element diff --git a/src/ptulsconv/docparser/tag_mapping.py b/src/ptulsconv/docparser/tag_mapping.py index bf5c6c1..f022be4 100644 --- a/src/ptulsconv/docparser/tag_mapping.py +++ b/src/ptulsconv/docparser/tag_mapping.py @@ -1,74 +1,81 @@ +from __future__ import annotations + import sys from enum import Enum -from typing import Optional, Callable, Any, List +from typing import Any, Callable + +# from ptulsconv.docparser.adr_entity import GenericEvent class TagMapping: class ContentSource(Enum): - Session = 1, - Track = 2, - Clip = 3, + Session = (1,) + Track = (2,) + Clip = (3,) source: str - alternate_source: Optional[ContentSource] + alternate_source: ContentSource | None formatter: Callable[[str], Any] @staticmethod def print_rules(for_type: object, output=sys.stdout): format_str = "%-20s | %-20s | %-25s" - hr = "%s+%s+%s" % ("-" * 21, "-" * 23, "-" * 26) - print("Tag mapping for %s" % for_type.__name__) + hr = f"{'-' * 21}+{'-' * 23}+{'-' * 26}" + print(f"Tag mapping for {for_type.__class__}") print(hr) - print(format_str % ("Tag Source", "Target", "Type"), - file=output) + print(format_str % ("Tag Source", "Target", "Type"), file=output) print(hr) for rule in for_type.tag_mapping: t = for_type.__annotations__[rule.target] - print(format_str % (rule.source, rule.target, t), - file=output) + print(format_str % (rule.source, rule.target, t), file=output) if rule.alternate_source is TagMapping.ContentSource.Session: - print(format_str % (" - (Session Name)", rule.target, t), - file=output) + print(format_str % (" - (Session Name)", rule.target, t), file=output) elif rule.alternate_source is TagMapping.ContentSource.Track: - print(format_str % (" - (Track Name)", rule.target, t), - file=output) + print(format_str % (" - (Track Name)", rule.target, t), file=output) elif rule.alternate_source is TagMapping.ContentSource.Clip: - print(format_str % (" - (Clip Name)", rule.target, t), - file=output) + print(format_str % (" - (Clip Name)", rule.target, t), file=output) @staticmethod - def apply_rules(rules: List['TagMapping'], - tags: dict, - clip_content: str, - track_content: str, - session_content: str, - to: object): + def apply_rules( + rules: list[TagMapping], + tags: dict, + clip_content: str, + track_content: str, + session_content: str, + to: object, + ): done = set() for rule in rules: if rule.target in done: continue - if rule.apply(tags, clip_content, track_content, session_content, - to): + if rule.apply(tags, clip_content, track_content, session_content, to): done.update(rule.target) - def __init__(self, source: str, - target: str, - alt: Optional[ContentSource] = None, - formatter=None): + def __init__( + self, + source: str, + target: str, + alt: ContentSource | None = None, + formatter=None, + ): self.source = source self.target = target self.alternate_source = alt self.formatter = formatter or (lambda x: x) - def apply(self, tags: dict, - clip_content: str, - track_content: str, - session_content: str, to: object) -> bool: + def apply( + self, + tags: dict, + clip_content: str, + track_content: str, + session_content: str, + to: object, + ) -> bool: new_value = None - if self.source in tags.keys(): + if self.source in tags: new_value = tags[self.source] elif self.alternate_source == TagMapping.ContentSource.Session: new_value = session_content diff --git a/src/ptulsconv/docparser/tagged_string_parser_visitor.py b/src/ptulsconv/docparser/tagged_string_parser_visitor.py index 44dd4cc..b11984a 100644 --- a/src/ptulsconv/docparser/tagged_string_parser_visitor.py +++ b/src/ptulsconv/docparser/tagged_string_parser_visitor.py @@ -1,13 +1,13 @@ -from parsimonious import NodeVisitor, Grammar -from typing import Dict from enum import Enum +from parsimonious import Grammar, NodeVisitor + class TagPreModes(Enum): - NORMAL = 'Normal' - APPEND = 'Append' - TIMESPAN = 'Timespan' - DIRECTIVE = 'Directive' + NORMAL = "Normal" + APPEND = "Append" + TIMESPAN = "Timespan" + DIRECTIVE = "Directive" tag_grammar = Grammar( @@ -36,7 +36,7 @@ def parse_tags(prompt: str) -> "TaggedStringResult": class TaggedStringResult: content: str - tag_dict: Dict[str, str] + tag_dict: dict[str, str] mode: TagPreModes def __init__(self, content, tag_dict, mode): @@ -46,16 +46,15 @@ class TaggedStringResult: class TagListVisitor(NodeVisitor): - @staticmethod def visit_document(_, visited_children) -> TaggedStringResult: modifier_opt, line_opt, _, tag_list_opt = visited_children - return TaggedStringResult(content=next(iter(line_opt), None), - tag_dict=next(iter(tag_list_opt), dict()), - mode=TagPreModes( - next(iter(modifier_opt), 'Normal')) - ) + return TaggedStringResult( + content=next(iter(line_opt), None), + tag_dict=next(iter(tag_list_opt), {}), + mode=TagPreModes(next(iter(modifier_opt), "Normal")), + ) @staticmethod def visit_line(node, _): @@ -63,18 +62,18 @@ class TagListVisitor(NodeVisitor): @staticmethod def visit_modifier(node, _): - if node.text.startswith('@'): + if node.text.startswith("@"): return TagPreModes.TIMESPAN - elif node.text.startswith('&'): + elif node.text.startswith("&"): return TagPreModes.APPEND - elif node.text.startswith('!'): + elif node.text.startswith("!"): return TagPreModes.DIRECTIVE else: return TagPreModes.NORMAL @staticmethod def visit_tag_list(_, visited_children): - retdict = dict() + retdict = {} for child in visited_children: if child[0] is not None: k, v = child[0] diff --git a/src/ptulsconv/footage.py b/src/ptulsconv/footage.py index a0f7f8b..978a34f 100644 --- a/src/ptulsconv/footage.py +++ b/src/ptulsconv/footage.py @@ -1,12 +1,14 @@ """ Methods for converting string reprentations of film footage. """ -from fractions import Fraction + +from __future__ import annotations + import re -from typing import Optional +from fractions import Fraction -def footage_to_seconds(footage: str) -> Optional[Fraction]: +def footage_to_seconds(footage: str) -> Fraction | None: """ Converts a string representation of a footage (35mm, 24fps) into a :class:`Fraction`, this fraction being a some number of @@ -15,7 +17,7 @@ def footage_to_seconds(footage: str) -> Optional[Fraction]: :param footage: A string reprenentation of a footage of the form resembling "90+01". """ - m = re.match(r'(\d+)\+(\d+)(\.\d+)?', footage) + m = re.match(r"(\d+)\+(\d+)(\.\d+)?", footage) if m is None: return None diff --git a/src/ptulsconv/pdf/__init__.py b/src/ptulsconv/pdf/__init__.py index 985ced9..5e4e3b0 100644 --- a/src/ptulsconv/pdf/__init__.py +++ b/src/ptulsconv/pdf/__init__.py @@ -1,16 +1,15 @@ +from __future__ import annotations + import datetime -from reportlab.pdfbase.pdfmetrics import (getAscent, getDescent) from reportlab.lib.units import inch +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.pdfmetrics import getAscent, getDescent +from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas from reportlab.platypus.doctemplate import BaseDocTemplate, PageTemplate from reportlab.platypus.frames import Frame -from reportlab.pdfbase import pdfmetrics -from reportlab.pdfbase.ttfonts import TTFont - -from typing import List - # TODO: A Generic report useful for spotting # TODO: A report useful for M&E mixer's notes # TODO: Use a default font that doesn't need to be installed @@ -27,7 +26,7 @@ class ReportCanvas(canvas.Canvas): def showPage(self): self._saved_page_states.append(dict(self.__dict__)) - self._startPage() + self._startPage() # pyright: ignore[reportAttributeAccessIssue] def save(self): """add page info to each page (page x of y)""" @@ -40,12 +39,16 @@ class ReportCanvas(canvas.Canvas): def draw_page_number(self, page_count): self.saveState() - self.setFont('Helvetica', 10) # FIXME make this customizable - self.drawString(0.5 * inch, 0.5 * inch, - "Page %d of %d" % (self._pageNumber, page_count)) - right_edge = self._pagesize[0] - 0.5 * inch - self.drawRightString(right_edge, 0.5 * inch, - self._report_date.strftime("%m/%d/%Y %H:%M")) + self.setFont("Helvetica", 10) # FIXME make this customizable + self.drawString( + 0.5 * inch, + 0.5 * inch, + f"Page {self._pageNumber} of {page_count}", # pyright: ignore[reportAttributeAccessIssue] + ) + right_edge = self._pagesize[0] - 0.5 * inch # pyright: ignore[reportAttributeAccessIssue] + self.drawRightString( + right_edge, 0.5 * inch, self._report_date.strftime("%m/%d/%Y %H:%M") + ) top_line = self.beginPath() top_line.moveTo(0.5 * inch, 0.75 * inch) @@ -60,49 +63,63 @@ class ADRDocTemplate(BaseDocTemplate): BaseDocTemplate.build(self, flowables, filename, canvasmaker) -def make_doc_template(page_size, filename, document_title, - title: str, - supervisor: str, - document_header: str, - client: str, - document_subheader: str, - left_margin=0.5 * inch, - fonts: List[TTFont] = []) -> ADRDocTemplate: - right_margin = top_margin = bottom_margin = 0.5 * inch - page_box = GRect(0., 0., page_size[0], page_size[1]) - _, page_box = page_box.split_x(left_margin, direction='l') - _, page_box = page_box.split_x(right_margin, direction='r') - _, page_box = page_box.split_y(bottom_margin, direction='u') - _, page_box = page_box.split_y(top_margin, direction='d') +def make_doc_template( + page_size: tuple[float, float], + filename, + document_title, + title: str, + supervisor: str, + document_header: str, + client: str, + document_subheader: str, + left_margin=0.5 * inch, + fonts: list[TTFont] | None = None, +) -> ADRDocTemplate: - footer_box, page_box = page_box.split_y(0.25 * inch, direction='u') - header_box, page_box = page_box.split_y(0.75 * inch, direction='d') - title_box, report_box = header_box.split_x(3.5 * inch, direction='r') + if fonts is None: + fonts = [] + + right_margin = top_margin = bottom_margin = 0.5 * inch + page_box: GRect = GRect(0.0, 0.0, page_size[0], page_size[1]) + _, page_box = page_box.split_x(left_margin, direction="l") # pyright: ignore[reportAssignmentType] + _, page_box = page_box.split_x(right_margin, direction="r") # pyright: ignore[reportAssignmentType] + _, page_box = page_box.split_y(bottom_margin, direction="u") # pyright: ignore[reportAssignmentType] + _, page_box = page_box.split_y(top_margin, direction="d") # pyright: ignore[reportAssignmentType] + + footer_box, page_box = page_box.split_y(0.25 * inch, direction="u") # pyright: ignore[reportAssignmentType] + header_box, page_box = page_box.split_y(0.75 * inch, direction="d") # pyright: ignore[reportAssignmentType] + title_box, report_box = header_box.split_x(3.5 * inch, direction="r") # pyright: ignore[reportOptionalMemberAccess] def on_page_lambda(c, _): - draw_header_footer(c, report_box, title_box, - footer_box, title=title, - supervisor=supervisor, - document_subheader=document_subheader, - client=client, - doc_title=document_header) + draw_header_footer( + c, + report_box, + title_box, + footer_box, + title=title, + supervisor=supervisor, + document_subheader=document_subheader, + client=client, + doc_title=document_header, + ) - frames = [Frame(page_box.min_x, page_box.min_y, - page_box.width, page_box.height)] + frames = [Frame(page_box.min_x, page_box.min_y, page_box.width, page_box.height)] - page_template = PageTemplate(id="Main", - frames=frames, - onPage=on_page_lambda) + page_template = PageTemplate(id="Main", frames=frames, onPage=on_page_lambda) for font in fonts: pdfmetrics.registerFont(font) - doc = ADRDocTemplate(filename, - title=document_title, - author=supervisor, - pagesize=page_size, - leftMargin=left_margin, rightMargin=right_margin, - topMargin=top_margin, bottomMargin=bottom_margin) + doc = ADRDocTemplate( + filename, + title=document_title, + author=supervisor, + pagesize=page_size, + leftMargin=left_margin, + rightMargin=right_margin, + topMargin=top_margin, + bottomMargin=bottom_margin, + ) doc.addPageTemplates([page_template]) @@ -115,27 +132,43 @@ def time_format(mins, zero_str="-"): """ if mins is None: return zero_str - if mins == 0. and zero_str is not None: + if mins == 0.0 and zero_str is not None: return zero_str - elif mins < 60.: - return "%im" % round(mins) + elif mins < 60.0: + return f"{round(mins)}m" else: m = round(mins) hh, mm = divmod(m, 60) - return "%i:%02i" % (hh, mm) + return f"{hh}:{mm:02}" -def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, - footer_box, title: str, supervisor: str, - document_subheader: str, client: str, doc_title="", - font_name='Helvetica'): +def draw_header_footer( + a_canvas: ReportCanvas, + left_box, + right_box, + footer_box, + title: str, + supervisor: str, + document_subheader: str, + client: str, + doc_title="", + font_name="Helvetica", +): - (_supervisor_box, client_box,), title_box = \ - right_box.divide_y([16., 16., ]) - title_box.draw_text_cell(a_canvas, title, font_name, 18, - inset_y=2., inset_x=5.) - client_box.draw_text_cell(a_canvas, client, font_name, 11, - inset_y=2., inset_x=5.) + ( + ( + _supervisor_box, + client_box, + ), + title_box, + ) = right_box.divide_y( + [ + 16.0, + 16.0, + ] + ) + title_box.draw_text_cell(a_canvas, title, font_name, 18, inset_y=2.0, inset_x=5.0) + client_box.draw_text_cell(a_canvas, client, font_name, 11, inset_y=2.0, inset_x=5.0) a_canvas.saveState() a_canvas.setLineWidth(0.5) @@ -150,20 +183,26 @@ def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, a_canvas.drawPath(tline2) a_canvas.restoreState() - (doc_title_cell, spotting_version_cell,), _ = \ - left_box.divide_y([18., 14], direction='d') + ( + ( + doc_title_cell, + spotting_version_cell, + ), + _, + ) = left_box.divide_y([18.0, 14], direction="d") - doc_title_cell.draw_text_cell(a_canvas, doc_title, font_name, 14., - inset_y=2.) + doc_title_cell.draw_text_cell(a_canvas, doc_title, font_name, 14.0, inset_y=2.0) if document_subheader is not None: - spotting_version_cell.draw_text_cell(a_canvas, document_subheader, - font_name, 12., inset_y=2.) + spotting_version_cell.draw_text_cell( + a_canvas, document_subheader, font_name, 12.0, inset_y=2.0 + ) if supervisor is not None: - a_canvas.setFont(font_name, 11.) - a_canvas.drawCentredString(footer_box.min_x + footer_box.width / 2., - footer_box.min_y, supervisor) + a_canvas.setFont(font_name, 11.0) + a_canvas.drawCentredString( + footer_box.min_x + footer_box.width / 2.0, footer_box.min_y, supervisor + ) class GRect: @@ -200,71 +239,75 @@ class GRect: return self.y + self.height / 2 def normalize(self): - if self.width < 0.: + if self.width < 0.0: self.width = abs(self.width) self.x = self.x - self.width - if self.height < 0.: + if self.height < 0.0: self.height = abs(self.height) self.y = self.y - self.height - def split_x(self, at, direction='l'): + def split_x(self, at, direction="l"): if at >= self.width: return None, self elif at <= 0: return self, None else: - if direction == 'l': - return (GRect(self.min_x, self.min_y, at, self.height), - GRect(self.min_x + at, self.y, - self.width - at, self.height)) + if direction == "l": + return ( + GRect(self.min_x, self.min_y, at, self.height), + GRect(self.min_x + at, self.y, self.width - at, self.height), + ) else: - return (GRect(self.max_x - at, self.y, at, self.height), - GRect(self.min_x, self.y, - self.width - at, self.height)) + return ( + GRect(self.max_x - at, self.y, at, self.height), + GRect(self.min_x, self.y, self.width - at, self.height), + ) - def split_y(self, at, direction='u'): + def split_y(self, at, direction="u"): if at >= self.height: return None, self elif at <= 0: return self, None else: - if direction == 'u': - return (GRect(self.x, self.y, self.width, at), - GRect(self.x, self.y + at, - self.width, self.height - at)) + if direction == "u": + return ( + GRect(self.x, self.y, self.width, at), + GRect(self.x, self.y + at, self.width, self.height - at), + ) else: - return (GRect(self.x, self.max_y - at, self.width, at), - GRect(self.x, self.y, - self.width, self.height - at)) + return ( + GRect(self.x, self.max_y - at, self.width, at), + GRect(self.x, self.y, self.width, self.height - at), + ) def inset_xy(self, dx, dy): - return GRect(self.x + dx, self.y + dy, - self.width - dx * 2, self.height - dy * 2) + return GRect( + self.x + dx, self.y + dy, self.width - dx * 2, self.height - dy * 2 + ) def inset(self, d): return self.inset_xy(d, d) def __repr__(self): - return "" % \ - (self.x, self.y, self.width, self.height) + return f"" - def divide_x(self, x_list, direction='l'): - ret_list = list() + def divide_x(self, x_list, direction="l"): + ret_list = [] rem = self for item in x_list: - s, rem = rem.split_x(item, direction) + s, rem = rem.split_x(item, direction) # pyright: ignore[reportOptionalMemberAccess] ret_list.append(s) return ret_list, rem - def divide_y(self, y_list, direction='u'): - ret_list = list() + def divide_y(self, y_list, direction="u"): + ret_list = [] rem = self for item in y_list: - s, rem = rem.split_y(item, direction) + s, rem = rem.split_y(item, direction) # pyright: ignore[reportOptionalMemberAccess] ret_list.append(s) return ret_list, rem @@ -279,18 +322,14 @@ class GRect: def draw_border(self, a_canvas, edge): def draw_border_impl(en): - if en == 'min_x': - coordinates = ((self.min_x, self.min_y), - (self.min_x, self.max_y)) - elif en == 'max_x': - coordinates = ((self.max_x, self.min_y), - (self.max_x, self.max_y)) - elif en == 'min_y': - coordinates = ((self.min_x, self.min_y), - (self.max_x, self.min_y)) - elif en == 'max_y': - coordinates = ((self.min_x, self.max_y), - (self.max_x, self.max_y)) + if en == "min_x": + coordinates = ((self.min_x, self.min_y), (self.min_x, self.max_y)) + elif en == "max_x": + coordinates = ((self.max_x, self.min_y), (self.max_x, self.max_y)) + elif en == "min_y": + coordinates = ((self.min_x, self.min_y), (self.max_x, self.min_y)) + elif en == "max_y": + coordinates = ((self.min_x, self.max_y), (self.max_x, self.max_y)) else: return @@ -305,9 +344,18 @@ class GRect: for e in edge: draw_border_impl(e) - def draw_text_cell(self, a_canvas, text, font_name, font_size, - vertical_align='t', force_baseline=None, inset_x=0., - inset_y=0., draw_baseline=False): + def draw_text_cell( + self, + a_canvas, + text, + font_name, + font_size, + vertical_align="t", + force_baseline=None, + inset_x=0.0, + inset_y=0.0, + draw_baseline=False, + ): if text is None: return @@ -315,9 +363,9 @@ class GRect: inset_rect = self.inset_xy(inset_x, inset_y) - if vertical_align == 'm': - y = inset_rect.center_y - getAscent(font_name, font_size) / 2. - elif vertical_align == 't': + if vertical_align == "m": + y = inset_rect.center_y - getAscent(font_name, font_size) / 2.0 + elif vertical_align == "t": y = inset_rect.max_y - getAscent(font_name, font_size) else: y = inset_rect.min_y - getDescent(font_name, font_size) @@ -339,14 +387,15 @@ class GRect: a_canvas.setDash([3.0, 1.0, 2.0, 1.0]) a_canvas.setLineWidth(0.5) bl = a_canvas.beginPath() - bl.moveTo(inset_rect.min_x, y - 1.) - bl.lineTo(inset_rect.max_x, y - 1.) + bl.moveTo(inset_rect.min_x, y - 1.0) + bl.lineTo(inset_rect.max_x, y - 1.0) a_canvas.drawPath(bl) a_canvas.restoreState() - def draw_flowable(self, a_canvas, flowable, inset_x=0., - inset_y=0., draw_baselines=False): + def draw_flowable( + self, a_canvas, flowable, inset_x=0.0, inset_y=0.0, draw_baselines=False + ): a_canvas.saveState() inset_rect = self.inset_xy(inset_x, inset_y) @@ -355,7 +404,7 @@ class GRect: cp.rect(self.min_x, self.min_y, self.width, self.height) a_canvas.clipPath(cp, stroke=0, fill=0) - w, h = flowable.wrap(inset_rect.width, inset_rect.height) + _w, h = flowable.wrap(inset_rect.width, inset_rect.height) flowable.drawOn(a_canvas, inset_rect.x, inset_rect.max_y - h) @@ -364,7 +413,7 @@ class GRect: a_canvas.setLineWidth(0.5) leading = flowable.style.leading - y = inset_rect.max_y - flowable.style.fontSize - 1. + y = inset_rect.max_y - flowable.style.fontSize - 1.0 while y > inset_rect.min_x: bl = a_canvas.beginPath() bl.moveTo(inset_rect.min_x, y) diff --git a/src/ptulsconv/pdf/continuity.py b/src/ptulsconv/pdf/continuity.py index a36affb..9c6641f 100644 --- a/src/ptulsconv/pdf/continuity.py +++ b/src/ptulsconv/pdf/continuity.py @@ -1,7 +1,6 @@ from fractions import Fraction -from typing import Tuple, List -from reportlab.lib.pagesizes import portrait, letter +from reportlab.lib.pagesizes import letter, portrait from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.units import inch from reportlab.platypus import Paragraph, Table @@ -10,47 +9,53 @@ from ptulsconv.broadcast_timecode import TimecodeFormat from ptulsconv.pdf import make_doc_template -# TODO: A Continuity - -def table_for_scene(scene, tc_format, font_name='Helvetica'): - scene_style = getSampleStyleSheet()['Normal'] +def table_for_scene(scene, tc_format, font_name="Helvetica"): + scene_style = getSampleStyleSheet()["Normal"] scene_style.fontName = font_name - scene_style.leftIndent = 0. - scene_style.leftPadding = 0. - scene_style.spaceAfter = 18. + scene_style.leftIndent = 0.0 + scene_style.leftPadding = 0.0 + scene_style.spaceAfter = 18.0 - tc_data = "%s
%s" % (tc_format.seconds_to_smpte(scene[2]), - tc_format.seconds_to_smpte(scene[3])) + tc_data = f"{tc_format.seconds_to_smpte(scene[2])}
{tc_format.seconds_to_smpte(scene[3])}" row = [ Paragraph(tc_data, scene_style), Paragraph(scene[1], scene_style), ] - style = [('VALIGN', (0, 0), (-1, -1), 'TOP'), - ('LEFTPADDING', (0, 0), (0, 0), 0.0), - ('BOTTOMPADDING', (0, 0), (-1, -1), 12.), - ('FONTNAME', (0, 0), (-1, -1), font_name)] + style = [ + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("LEFTPADDING", (0, 0), (0, 0), 0.0), + ("BOTTOMPADDING", (0, 0), (-1, -1), 12.0), + ("FONTNAME", (0, 0), (-1, -1), font_name), + ] return Table(data=[row], style=style, colWidths=[1.0 * inch, 6.5 * inch]) -def output_report(scenes: List[Tuple[str, str, Fraction, Fraction]], - tc_display_format: TimecodeFormat, - title: str, client: str, supervisor, paper_size=letter): - filename = "%s Continuity.pdf" % title +def output_report( + scenes: list[tuple[str, str, Fraction, Fraction]], + tc_display_format: TimecodeFormat, + title: str, + client: str, + supervisor, + paper_size=letter, +): + filename = f"{title} Continuity.pdf" document_header = "Continuity" - doc = make_doc_template(page_size=portrait(paper_size), - filename=filename, - document_title="Continuity", - title=title, - client=client, - document_subheader="", - supervisor=supervisor, - document_header=document_header, - left_margin=0.5 * inch) - story = list() + doc = make_doc_template( + page_size=portrait(paper_size), + filename=filename, + document_title="Continuity", + title=title, + client=client, + document_subheader="", + supervisor=supervisor, + document_header=document_header, + left_margin=0.5 * inch, + ) + story = [] # story.append(Spacer(height=0.5 * inch, width=1.)) for scene in scenes: story.append(table_for_scene(scene, tc_display_format)) diff --git a/src/ptulsconv/pdf/line_count.py b/src/ptulsconv/pdf/line_count.py index 7563021..4ed3d66 100644 --- a/src/ptulsconv/pdf/line_count.py +++ b/src/ptulsconv/pdf/line_count.py @@ -1,213 +1,257 @@ -from typing import List, Optional +from __future__ import annotations -# from reportlab.pdfbase import pdfmetrics -# from reportlab.pdfbase.ttfonts import TTFont - -from reportlab.lib.units import inch -from reportlab.lib.pagesizes import letter, portrait from reportlab.lib import colors - -from reportlab.platypus import Table, Paragraph, Spacer +from reportlab.lib.pagesizes import letter, portrait from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.units import inch +from reportlab.platypus import Paragraph, Spacer, Table -from .__init__ import time_format, make_doc_template from ..docparser.adr_entity import ADRLine +from .__init__ import make_doc_template, time_format -def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]], - show_priorities=False, include_omitted=False): - columns = list() - reel_numbers = reel_list or sorted( - set([x.reel for x in lines if x.reel is not None]) +def build_columns( + lines: list[ADRLine], + reel_list: list[str] | None, + show_priorities=False, + include_omitted=False, +): + columns = [] + reel_numbers = reel_list or sorted({x.reel for x in lines if x.reel is not None}) + + num_column_width = 15.0 / 32.0 * inch + + columns.append( + { + "heading": "#", + "value_getter": lambda recs: recs[0].character_id, + "value_getter2": lambda recs: "", + "style_getter": lambda col_index: [], + "width": 0.375 * inch, + "summarize": False, + } ) - num_column_width = 15. / 32. * inch + columns.append( + { + "heading": "Role", + "value_getter": lambda recs: recs[0].character_name, + "value_getter2": lambda recs: recs[0].actor_name or "", + "style_getter": lambda col_index: [ + ("LINEAFTER", (col_index, 0), (col_index, -1), 1.0, colors.black) + ], + "width": 1.75 * inch, + "summarize": False, + } + ) - columns.append({ - 'heading': '#', - 'value_getter': lambda recs: recs[0].character_id, - 'value_getter2': lambda recs: "", - 'style_getter': lambda col_index: [], - 'width': 0.375 * inch, - 'summarize': False - }) + columns.append( + { + "heading": "TV", + "value_getter": lambda recs: len([r for r in recs if r.tv]), + "value_getter2": ( + lambda recs: time_format( + sum([r.time_budget_mins or 0.0 for r in recs if r.tv]) + ) + ), + "style_getter": ( + lambda col_index: [ + ("ALIGN", (col_index, 0), (col_index, -1), "CENTER"), + ("LINEBEFORE", (col_index, 0), (col_index, -1), 1.0, colors.black), + ("LINEAFTER", (col_index, 0), (col_index, -1), 0.5, colors.gray), + ] + ), + "width": num_column_width, + } + ) - columns.append({ - 'heading': 'Role', - 'value_getter': lambda recs: recs[0].character_name, - 'value_getter2': lambda recs: recs[0].actor_name or "", - 'style_getter': lambda col_index: [('LINEAFTER', - (col_index, 0), - (col_index, -1), - 1.0, colors.black)], - 'width': 1.75 * inch, - 'summarize': False - }) + columns.append( + { + "heading": "Opt", + "value_getter": lambda recs: len([r for r in recs if r.optional]), + "value_getter2": ( + lambda recs: time_format( + sum([r.time_budget_mins or 0.0 for r in recs if r.optional]) + ) + ), + "style_getter": ( + lambda col_index: [ + ("ALIGN", (col_index, 0), (col_index, -1), "CENTER"), + ("LINEAFTER", (col_index, 0), (col_index, -1), 0.5, colors.gray), + ] + ), + "width": num_column_width, + } + ) - columns.append({ - 'heading': 'TV', - 'value_getter': lambda recs: len([r for r in recs if r.tv]), - 'value_getter2': (lambda recs: - time_format(sum([r.time_budget_mins or 0. - for r in recs if r.tv])) - ), - 'style_getter': (lambda col_index: - [('ALIGN', (col_index, 0), (col_index, -1), - 'CENTER'), - ('LINEBEFORE', (col_index, 0), (col_index, -1), - 1., colors.black), - ('LINEAFTER', (col_index, 0), (col_index, -1), - .5, colors.gray)] - ), - 'width': num_column_width - }) + columns.append( + { + "heading": "Eff", + "value_getter": lambda recs: len([r for r in recs if r.effort]), + "value_getter2": ( + lambda recs: time_format( + sum([r.time_budget_mins or 0.0 for r in recs if r.effort]) + ) + ), + "style_getter": ( + lambda col_index: [("ALIGN", (col_index, 0), (col_index, -1), "CENTER")] + ), + "width": num_column_width, + } + ) - columns.append({ - 'heading': 'Opt', - 'value_getter': lambda recs: len([r for r in recs if r.optional]), - 'value_getter2': (lambda recs: - time_format(sum([r.time_budget_mins or 0. - for r in recs if r.optional])) - ), - 'style_getter': (lambda col_index: - [('ALIGN', (col_index, 0), (col_index, -1), - 'CENTER'), - ('LINEAFTER', (col_index, 0), (col_index, -1), - .5, colors.gray)] - ), - 'width': num_column_width - }) - - columns.append({ - 'heading': 'Eff', - 'value_getter': lambda recs: len([r for r in recs if r.effort]), - 'value_getter2': (lambda recs: - time_format(sum([r.time_budget_mins or 0. - for r in recs if r.effort])) - ), - 'style_getter': (lambda col_index: - [('ALIGN', (col_index, 0), (col_index, -1), - 'CENTER')] - ), - 'width': num_column_width - }) - - columns.append({ - 'heading': '', - 'value_getter': lambda _: '', - 'value_getter2': lambda _: '', - 'style_getter': lambda col_index: [ - ('LINEBEFORE', (col_index, 0), (col_index, -1), 1., colors.black), - ('LINEAFTER', (col_index, 0), (col_index, -1), 1., colors.black), - ], - 'width': 2. - }) + columns.append( + { + "heading": "", + "value_getter": lambda _: "", + "value_getter2": lambda _: "", + "style_getter": lambda col_index: [ + ("LINEBEFORE", (col_index, 0), (col_index, -1), 1.0, colors.black), + ("LINEAFTER", (col_index, 0), (col_index, -1), 1.0, colors.black), + ], + "width": 2.0, + } + ) if len(reel_numbers) > 0: - for n in reel_numbers: - columns.append({ - 'heading': n, - 'value_getter': (lambda recs, n1=n: - len([r for r in recs if r.reel == n1]) - ), - 'value_getter2': (lambda recs, n1=n: - time_format(sum([r.time_budget_mins or 0. - for r in recs - if r.reel == n1])) - ), - 'style_getter': (lambda col_index: - [('ALIGN', (col_index, 0), (col_index, -1), - 'CENTER'), - ('LINEAFTER', (col_index, 0), - (col_index, -1), - .5, colors.gray)] - ), - - 'width': num_column_width - }) + columns.append( + { + "heading": n, + "value_getter": ( + lambda recs, n1=n: len([r for r in recs if r.reel == n1]) + ), + "value_getter2": ( + lambda recs, n1=n: time_format( + sum( + [ + r.time_budget_mins or 0.0 + for r in recs + if r.reel == n1 + ] + ) + ) + ), + "style_getter": ( + lambda col_index: [ + ("ALIGN", (col_index, 0), (col_index, -1), "CENTER"), + ( + "LINEAFTER", + (col_index, 0), + (col_index, -1), + 0.5, + colors.gray, + ), + ] + ), + "width": num_column_width, + } + ) if show_priorities: - for n in range(1, 6,): - columns.append({ - 'heading': 'P%i' % n, - 'value_getter': lambda recs: len([r for r in recs - if r.priority == n]), - 'value_getter2': (lambda recs: - time_format(sum([r.time_budget_mins or 0. - for r in recs - if r.priority == n])) - ), - 'style_getter': lambda col_index: [], - 'width': num_column_width - }) + for n in range( + 1, + 6, + ): + columns.append( + { + "heading": f"P{n}", + "value_getter": lambda recs, N=n: len( + [r for r in recs if r.priority == N] + ), + "value_getter2": ( + lambda recs, N=n: time_format( + sum( + [ + r.time_budget_mins or 0.0 + for r in recs + if r.priority == N + ] + ) + ) + ), + "style_getter": lambda col_index: [], + "width": num_column_width, + } + ) - columns.append({ - 'heading': '>P5', - 'value_getter': lambda recs: len([r for r in recs - if (r.priority or 5) > 5]), - 'value_getter2': (lambda recs: - time_format(sum([r.time_budget_mins or 0. - for r in recs - if (r.priority or 5) > 5])) - ), - 'style_getter': lambda col_index: [], - 'width': num_column_width - }) + columns.append( + { + "heading": ">P5", + "value_getter": lambda recs: len( + [r for r in recs if (r.priority or 5) > 5] + ), + "value_getter2": ( + lambda recs: time_format( + sum( + [ + r.time_budget_mins or 0.0 + for r in recs + if (r.priority or 5) > 5 + ] + ) + ) + ), + "style_getter": lambda col_index: [], + "width": num_column_width, + } + ) if include_omitted: - columns.append({ - 'heading': 'Omit', - 'value_getter': lambda recs: len([r for r in recs if r.omitted]), - 'value_getter2': (lambda recs: - time_format(sum([r.time_budget_mins or 0. - for r in recs if r.omitted]))), - 'style_getter': (lambda col_index: - [('ALIGN', (col_index, 0), (col_index, -1), - 'CENTER')] - ), - 'width': num_column_width - }) + columns.append( + { + "heading": "Omit", + "value_getter": lambda recs: len([r for r in recs if r.omitted]), + "value_getter2": ( + lambda recs: time_format( + sum([r.time_budget_mins or 0.0 for r in recs if r.omitted]) + ) + ), + "style_getter": ( + lambda col_index: [ + ("ALIGN", (col_index, 0), (col_index, -1), "CENTER") + ] + ), + "width": num_column_width, + } + ) - columns.append({ - 'heading': 'Total', - 'value_getter': lambda recs: len([r for r in recs if not r.omitted]), - 'value_getter2': (lambda recs: - time_format( - sum([r.time_budget_mins or 0. - - for r in recs if not r.omitted]) - ) - ), - 'style_getter': (lambda col_index: - [('LINEBEFORE', (col_index, 0), (col_index, -1), - 1.0, colors.black), - ('ALIGN', (col_index, 0), (col_index, -1), - 'CENTER')] - ), - 'width': 0.5 * inch - }) + columns.append( + { + "heading": "Total", + "value_getter": lambda recs: len([r for r in recs if not r.omitted]), + "value_getter2": ( + lambda recs: time_format( + sum([r.time_budget_mins or 0.0 for r in recs if not r.omitted]) + ) + ), + "style_getter": ( + lambda col_index: [ + ("LINEBEFORE", (col_index, 0), (col_index, -1), 1.0, colors.black), + ("ALIGN", (col_index, 0), (col_index, -1), "CENTER"), + ] + ), + "width": 0.5 * inch, + } + ) return columns -def populate_columns(lines: List[ADRLine], columns, include_omitted, - _page_size): - data = list() - styles = list() - columns_widths = list() +def populate_columns(lines: list[ADRLine], columns, include_omitted, _page_size): + data = [] + styles = [] + columns_widths = [] - sorted_character_numbers: List[str] = sorted( - set([x.character_id for x in lines]), - key=lambda x: str(x)) - - # construct column styles + sorted_character_numbers: list[str | None] = sorted( + {x.character_id for x in lines}, key=lambda x: str(x) + ) for i, c in enumerate(columns): - styles.extend(c['style_getter'](i)) - columns_widths.append(c['width']) + styles.extend(c["style_getter"](i)) + columns_widths.append(c["width"]) - data.append(list(map(lambda x: x['heading'], columns))) + data.append([x["heading"] for x in columns]) if not include_omitted: lines = [x for x in lines if not x.omitted] @@ -215,37 +259,44 @@ def populate_columns(lines: List[ADRLine], columns, include_omitted, for n in sorted_character_numbers: char_records = [x for x in lines if x.character_id == n] if len(char_records) > 0: - row_data = list() - row_data2 = list() + row_data = [] + row_data2 = [] for col in columns: row1_index = len(data) row2_index = row1_index + 1 - row_data.append(col['value_getter'](list(char_records))) - row_data2.append(col['value_getter2'](list(char_records))) + row_data.append(col["value_getter"](list(char_records))) + row_data2.append(col["value_getter2"](list(char_records))) - styles.extend([('TEXTCOLOR', (0, row2_index), (-1, row2_index), - colors.red), - ('LINEBELOW', (0, row2_index), (-1, row2_index), - 0.5, colors.black)]) + styles.extend( + [ + ("TEXTCOLOR", (0, row2_index), (-1, row2_index), colors.red), + ( + "LINEBELOW", + (0, row2_index), + (-1, row2_index), + 0.5, + colors.black, + ), + ] + ) data.append(row_data) data.append(row_data2) - summary_row1 = list() - summary_row2 = list() + summary_row1 = [] + summary_row2 = [] row1_index = len(data) for col in columns: - if col.get('summarize', True): - summary_row1.append(col['value_getter'](lines)) - summary_row2.append(col['value_getter2'](lines)) + if col.get("summarize", True): + summary_row1.append(col["value_getter"](lines)) + summary_row2.append(col["value_getter2"](lines)) else: summary_row1.append("") summary_row2.append("") - styles.append(('LINEABOVE', (0, row1_index), (-1, row1_index), 2.0, - colors.black)) + styles.append(("LINEABOVE", (0, row1_index), (-1, row1_index), 2.0, colors.black)) data.append(summary_row1) data.append(summary_row2) @@ -257,29 +308,41 @@ def populate_columns(lines: List[ADRLine], columns, include_omitted, # pass -def output_report(lines: List[ADRLine], reel_list: List[str], - include_omitted=False, page_size=portrait(letter), - font_name='Helvetica'): - columns = build_columns(lines, include_omitted=include_omitted, - reel_list=reel_list) - data, style, columns_widths = populate_columns(lines, columns, - include_omitted, page_size) +def output_report( + lines: list[ADRLine], + reel_list: list[str], + page_size: tuple[float, float] | None = None, + include_omitted=False, + font_name="Helvetica", +): + if page_size is None: + page_size = portrait(letter) - style.append(('FONTNAME', (0, 0), (-1, -1), font_name)) - style.append(('FONTSIZE', (0, 0), (-1, -1), 9.)) - style.append(('LINEBELOW', (0, 0), (-1, 0), 1.0, colors.black)) + columns = build_columns(lines, include_omitted=include_omitted, reel_list=reel_list) + data, style, columns_widths = populate_columns( + lines, columns, include_omitted, page_size + ) + + style.append(("FONTNAME", (0, 0), (-1, -1), font_name)) + style.append(("FONTSIZE", (0, 0), (-1, -1), 9.0)) + style.append(("LINEBELOW", (0, 0), (-1, 0), 1.0, colors.black)) # style.append(('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.gray)) # pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) - title = "%s Line Count" % lines[0].title - filename = title + '.pdf' - doc = make_doc_template(page_size=page_size, filename=filename, - document_title=title, title=lines[0].title, - document_subheader=lines[0].spot, - client=lines[0].client, - supervisor=lines[0].supervisor, - document_header='Line Count') + title = f"{lines[0].title} Line Count" + filename = title + ".pdf" + + doc = make_doc_template( + page_size=page_size, + filename=filename, + document_title=title, + title=lines[0].title, + document_subheader=lines[0].spot or "", + client=lines[0].client or "", + supervisor=lines[0].supervisor or "", + document_header="Line Count", + ) # header_data, header_style, header_widths = build_header(columns_widths) # header_table = Table(data=header_data, style=header_style, @@ -287,18 +350,17 @@ def output_report(lines: List[ADRLine], reel_list: List[str], table = Table(data=data, style=style, colWidths=columns_widths) - story = [Spacer(height=0.5 * inch, width=1.), table] + story = [Spacer(height=0.5 * inch, width=1.0), table] - style = getSampleStyleSheet()['Normal'] + style = getSampleStyleSheet()["Normal"] style.fontName = font_name - style.fontSize = 12. - style.spaceBefore = 16. - style.spaceAfter = 16. + style.fontSize = 12.0 + style.spaceBefore = 16.0 + style.spaceAfter = 16.0 omitted_count = len([x for x in lines if x.omitted]) if not include_omitted and omitted_count > 0: - story.append(Paragraph("* %i Omitted lines are excluded." % - omitted_count, style)) + story.append(Paragraph(f"* {omitted_count} Omitted lines are excluded.", style)) doc.build(story) diff --git a/src/ptulsconv/pdf/recordist_log.py b/src/ptulsconv/pdf/recordist_log.py index 38f2245..4451755 100644 --- a/src/ptulsconv/pdf/recordist_log.py +++ b/src/ptulsconv/pdf/recordist_log.py @@ -1,5 +1,6 @@ # TODO: Complete Recordist Log + def output_report(records): # order by start diff --git a/src/ptulsconv/pdf/summary_log.py b/src/ptulsconv/pdf/summary_log.py index c66df6e..ee5da11 100644 --- a/src/ptulsconv/pdf/summary_log.py +++ b/src/ptulsconv/pdf/summary_log.py @@ -1,19 +1,18 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations -from .__init__ import time_format, make_doc_template -from reportlab.lib.units import inch from reportlab.lib.pagesizes import letter, portrait - -from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.units import inch +from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table -from typing import List -from ptulsconv.docparser.adr_entity import ADRLine from ptulsconv.broadcast_timecode import TimecodeFormat +from ptulsconv.docparser.adr_entity import ADRLine + +from .__init__ import make_doc_template, time_format def build_aux_data_field(line: ADRLine): - entries = list() + entries = [] if line.reason is not None: entries.append("Reason: " + line.reason) if line.note is not None: @@ -23,78 +22,91 @@ def build_aux_data_field(line: ADRLine): if line.shot is not None: entries.append("Shot: " + line.shot) - fg_color = 'white' + fg_color = "white" tag_field = "" if line.effort: - bg_color = 'red' - tag_field += "%s " \ - % (bg_color, fg_color, "EFF") + bg_color = "red" + tag_field += ( + f"EFF " + ) elif line.tv: - bg_color = 'blue' - tag_field += "%s " \ - % (bg_color, fg_color, "TV") + bg_color = "blue" + tag_field += ( + f"TV " + ) elif line.adlib: - bg_color = 'purple' - tag_field += "%s " \ - % (bg_color, fg_color, "ADLIB") + bg_color = "purple" + tag_field += ( + f"ADLIB " + ) elif line.optional: - bg_color = 'green' - tag_field += "%s" \ - % (bg_color, fg_color, "OPTIONAL") + bg_color = "green" + tag_field += f"font backColor={bg_color} textColor={fg_color} fontSize=11>OPTIONAL" entries.append(tag_field) return "
".join(entries) -def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, - font_name='Helvetica'): - story = list() +def build_story(lines: list[ADRLine], tc_rate: TimecodeFormat, font_name="Helvetica"): + story = [] this_scene = None - scene_style = getSampleStyleSheet()['Normal'] + scene_style = getSampleStyleSheet()["Normal"] scene_style.fontName = font_name - scene_style.leftIndent = 0. - scene_style.leftPadding = 0. - scene_style.spaceAfter = 18. - line_style = getSampleStyleSheet()['Normal'] + scene_style.leftIndent = 0.0 + scene_style.leftPadding = 0.0 + scene_style.spaceAfter = 18.0 + line_style = getSampleStyleSheet()["Normal"] line_style.fontName = font_name for line in lines: - table_style = [('VALIGN', (0, 0), (-1, -1), 'TOP'), - ('LEFTPADDING', (0, 0), (0, 0), 0.0), - ('BOTTOMPADDING', (0, 0), (-1, -1), 24.)] + table_style = [ + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("LEFTPADDING", (0, 0), (0, 0), 0.0), + ("BOTTOMPADDING", (0, 0), (-1, -1), 24.0), + ] - cue_number_field = "%s
%s" \ - % (line.cue_number, line.character_name) + cue_number_field = ( + f"{line.cue_number}
{line.character_name}" + ) time_data = time_format(line.time_budget_mins) if line.priority is not None: - time_data = time_data + "
" + "P: " + line.priority + time_data = time_data + "
" + "P: " + str(line.priority) aux_data_field = build_aux_data_field(line) tc_data = build_tc_data(line, tc_rate) - line_table_data = [[Paragraph(cue_number_field, line_style), - Paragraph(tc_data, line_style), - Paragraph(line.prompt, line_style), - Paragraph(time_data, line_style), - Paragraph(aux_data_field, line_style) - ]] + line_table_data = [ + [ + Paragraph(cue_number_field, line_style), + Paragraph(tc_data, line_style), + Paragraph(line.prompt or "[No Prompt]", line_style), + Paragraph(time_data, line_style), + Paragraph(aux_data_field, line_style), + ] + ] - line_table = Table(data=line_table_data, - colWidths=[inch * 0.75, inch, inch * 3., 0.5 * inch, - inch * 2.], - style=table_style) + line_table = Table( + data=line_table_data, + colWidths=[inch * 0.75, inch, inch * 3.0, 0.5 * inch, inch * 2.0], + style=table_style, + ) if (line.scene or "[No Scene]") != this_scene: this_scene = line.scene or "[No Scene]" - story.append(KeepTogether([ - Spacer(1., 0.25 * inch), - Paragraph("" + this_scene + "", scene_style), - line_table])) + story.append( + KeepTogether( + [ + Spacer(1.0, 0.25 * inch), + Paragraph("" + this_scene + "", scene_style), + line_table, + ] + ) + ) else: line_table.setStyle(table_style) story.append(KeepTogether([line_table])) @@ -103,31 +115,38 @@ def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, def build_tc_data(line: ADRLine, tc_format: TimecodeFormat): - tc_data = tc_format.seconds_to_smpte(line.start) + "
" + \ - tc_format.seconds_to_smpte(line.finish) + tc_data = ( + tc_format.seconds_to_smpte(line.start) + + "
" + + tc_format.seconds_to_smpte(line.finish) + ) third_line = [] if line.reel is not None: - if line.reel[0:1] == 'R': - third_line.append("%s" % line.reel) + if line.reel[0:1] == "R": + third_line.append(f"{line.reel}") else: - third_line.append("Reel %s" % line.reel) + third_line.append(f"Reel {line.reel}") if line.version is not None: - third_line.append("(%s)" % line.version) + third_line.append(f"({line.version})") if len(third_line) > 0: tc_data = tc_data + "
" + " ".join(third_line) return tc_data -def generate_report(page_size, lines: List[ADRLine], tc_rate: TimecodeFormat, - character_number=None, include_omitted=True): +def generate_report( + page_size, + lines: list[ADRLine], + tc_rate: TimecodeFormat, + character_number=None, + include_omitted=True, +): if character_number is not None: lines = [r for r in lines if r.character_id == character_number] - title = "%s ADR Report (%s)" % (lines[0].title, - lines[0].character_name) - document_header = "%s ADR Report" % lines[0].character_name + title = f"{lines[0].title} ADR Report ({lines[0].character_name})" + document_header = f"{lines[0].character_name} ADR Report" else: - title = "%s ADR Report" % lines[0].title - document_header = 'ADR Report' + title = f"{lines[0].title} ADR Report" + document_header = "ADR Report" if not include_omitted: lines = [line for line in lines if not line.omitted] @@ -135,22 +154,32 @@ def generate_report(page_size, lines: List[ADRLine], tc_rate: TimecodeFormat, lines = sorted(lines, key=lambda line: line.start) filename = title + ".pdf" - doc = make_doc_template(page_size=page_size, - filename=filename, document_title=title, - document_header=document_header, - title=lines[0].title, - supervisor=lines[0].supervisor, - client=lines[0].client, - document_subheader=lines[0].spot, - left_margin=0.75 * inch) + doc = make_doc_template( + page_size=page_size, + filename=filename, + document_title=title, + document_header=document_header, + title=lines[0].title, + supervisor=lines[0].supervisor or "", + client=lines[0].client or "", + document_subheader=lines[0].spot or "", + left_margin=0.75 * inch, + ) story = build_story(lines, tc_rate) doc.build(story) -def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat, - page_size=portrait(letter), by_character=False): +def output_report( + lines: list[ADRLine], + tc_display_format: TimecodeFormat, + page_size: tuple[float, float] | None = None, + by_character: bool = False, +): + if page_size is None: + page_size = portrait(letter) + if by_character: - character_numbers = set((r.character_id for r in lines)) + character_numbers = {r.character_id for r in lines} for n in character_numbers: generate_report(page_size, lines, tc_display_format, n) else: diff --git a/src/ptulsconv/pdf/supervisor_1pg.py b/src/ptulsconv/pdf/supervisor_1pg.py index 45c9733..1dd34ad 100644 --- a/src/ptulsconv/pdf/supervisor_1pg.py +++ b/src/ptulsconv/pdf/supervisor_1pg.py @@ -1,118 +1,151 @@ -from reportlab.pdfgen.canvas import Canvas +import datetime + +from reportlab.lib.pagesizes import letter +from reportlab.lib.styles import getSampleStyleSheet # from reportlab.pdfbase import pdfmetrics # from reportlab.pdfbase.ttfonts import TTFont - from reportlab.lib.units import inch -from reportlab.lib.pagesizes import letter - -from reportlab.lib.styles import getSampleStyleSheet +from reportlab.pdfgen.canvas import Canvas from reportlab.platypus import Paragraph -from .__init__ import GRect - from ptulsconv.broadcast_timecode import TimecodeFormat from ptulsconv.docparser.adr_entity import ADRLine -import datetime +from .__init__ import GRect -font_name = 'Helvetica' +font_name = "Helvetica" def draw_header_block(canvas, rect, record: ADRLine): - rect.draw_text_cell(canvas, record.cue_number, "Helvetica", 44, - vertical_align='m') + rect.draw_text_cell(canvas, record.cue_number, "Helvetica", 44, vertical_align="m") def draw_character_row(canvas, rect, record: ADRLine): label_frame, value_frame = rect.split_x(1.25 * inch) - label_frame.draw_text_cell(canvas, "CHARACTER", font_name, 10, - force_baseline=9.) - line = "%s / %s" % (record.character_id, record.character_name) + label_frame.draw_text_cell(canvas, "CHARACTER", font_name, 10, force_baseline=9.0) + line = f"{record.character_id} / {record.character_name}" if record.actor_name is not None: line = line + " / " + record.actor_name - value_frame.draw_text_cell(canvas, line, font_name, 12, force_baseline=9.) - rect.draw_border(canvas, ['min_y', 'max_y']) + value_frame.draw_text_cell(canvas, line, font_name, 12, force_baseline=9.0) + rect.draw_border(canvas, ["min_y", "max_y"]) def draw_cue_number_block(canvas, rect, record: ADRLine): - (label_frame, number_frame,), aux_frame = \ - rect.divide_y([0.20 * inch, 0.375 * inch], direction='d') - label_frame.draw_text_cell(canvas, "CUE NUMBER", font_name, 10, - inset_y=5., vertical_align='t') - number_frame.draw_text_cell(canvas, record.cue_number, font_name, 14, - inset_x=10., inset_y=2., draw_baseline=True) + ( + ( + label_frame, + number_frame, + ), + aux_frame, + ) = rect.divide_y([0.20 * inch, 0.375 * inch], direction="d") + label_frame.draw_text_cell( + canvas, "CUE NUMBER", font_name, 10, inset_y=5.0, vertical_align="t" + ) + number_frame.draw_text_cell( + canvas, + record.cue_number, + font_name, + 14, + inset_x=10.0, + inset_y=2.0, + draw_baseline=True, + ) - tags = {'tv': 'TV', - 'optional': 'OPT', - 'adlib': 'ADLIB', - 'effort': 'EFF', - 'tbw': 'TBW', - 'omitted': 'OMIT'} + tags = { + "tv": "TV", + "optional": "OPT", + "adlib": "ADLIB", + "effort": "EFF", + "tbw": "TBW", + "omitted": "OMIT", + } tag_field = "" - for key in tags.keys(): + for key, value in tags.items(): if getattr(record, key): - tag_field = tag_field + tags[key] + " " + tag_field = tag_field + value + " " - aux_frame.draw_text_cell(canvas, tag_field, font_name, 10, - inset_x=10., inset_y=2., vertical_align='t') - rect.draw_border(canvas, 'max_x') + aux_frame.draw_text_cell( + canvas, tag_field, font_name, 10, inset_x=10.0, inset_y=2.0, vertical_align="t" + ) + rect.draw_border(canvas, "max_x") -def draw_timecode_block(canvas, rect, record: ADRLine, - tc_display_format: TimecodeFormat): +def draw_timecode_block( + canvas, rect, record: ADRLine, tc_display_format: TimecodeFormat +): (in_label_frame, in_frame, out_label_frame, out_frame), _ = rect.divide_y( - [0.20 * inch, 0.25 * inch, 0.20 * inch, 0.25 * inch], direction='d') + [0.20 * inch, 0.25 * inch, 0.20 * inch, 0.25 * inch], direction="d" + ) - in_label_frame.draw_text_cell(canvas, "IN", font_name, 10, - vertical_align='t', inset_y=5., inset_x=5.) - in_frame.draw_text_cell(canvas, - tc_display_format.seconds_to_smpte(record.start), - font_name, 14, - inset_x=10., inset_y=2., - draw_baseline=True) - out_label_frame.draw_text_cell(canvas, "OUT", font_name, 10, - vertical_align='t', inset_y=5., inset_x=5.) - out_frame.draw_text_cell(canvas, - tc_display_format.seconds_to_smpte(record.finish), - font_name, 14, - inset_x=10., inset_y=2., - draw_baseline=True) + in_label_frame.draw_text_cell( + canvas, "IN", font_name, 10, vertical_align="t", inset_y=5.0, inset_x=5.0 + ) + in_frame.draw_text_cell( + canvas, + tc_display_format.seconds_to_smpte(record.start), + font_name, + 14, + inset_x=10.0, + inset_y=2.0, + draw_baseline=True, + ) + out_label_frame.draw_text_cell( + canvas, "OUT", font_name, 10, vertical_align="t", inset_y=5.0, inset_x=5.0 + ) + out_frame.draw_text_cell( + canvas, + tc_display_format.seconds_to_smpte(record.finish), + font_name, + 14, + inset_x=10.0, + inset_y=2.0, + draw_baseline=True, + ) - rect.draw_border(canvas, 'max_x') + rect.draw_border(canvas, "max_x") def draw_reason_block(canvas, rect, record: ADRLine): - reason_cell, notes_cell = rect.split_y(24., direction='d') - reason_label, reason_value = reason_cell.split_x(.75 * inch) - notes_label, notes_value = notes_cell.split_x(.75 * inch) + reason_cell, notes_cell = rect.split_y(24.0, direction="d") + reason_label, reason_value = reason_cell.split_x(0.75 * inch) + notes_label, notes_value = notes_cell.split_x(0.75 * inch) - reason_label.draw_text_cell(canvas, "Reason:", font_name, 12, - inset_x=5., inset_y=5., vertical_align='b') - reason_value.draw_text_cell(canvas, record.reason or "", font_name, 12, - inset_x=5., inset_y=5., draw_baseline=True, - vertical_align='b') - notes_label.draw_text_cell(canvas, "Note:", font_name, 12, - inset_x=5., inset_y=5., vertical_align='t') + reason_label.draw_text_cell( + canvas, "Reason:", font_name, 12, inset_x=5.0, inset_y=5.0, vertical_align="b" + ) + reason_value.draw_text_cell( + canvas, + record.reason or "", + font_name, + 12, + inset_x=5.0, + inset_y=5.0, + draw_baseline=True, + vertical_align="b", + ) + notes_label.draw_text_cell( + canvas, "Note:", font_name, 12, inset_x=5.0, inset_y=5.0, vertical_align="t" + ) - style = getSampleStyleSheet()['BodyText'] + style = getSampleStyleSheet()["BodyText"] style.fontName = font_name style.fontSize = 12 style.leading = 14 p = Paragraph(record.note or "", style) - notes_value.draw_flowable(canvas, p, draw_baselines=True, - inset_x=5., inset_y=5.) + notes_value.draw_flowable(canvas, p, draw_baselines=True, inset_x=5.0, inset_y=5.0) def draw_prompt(canvas, rect, prompt=""): - label, block = rect.split_y(0.20 * inch, direction='d') + label, block = rect.split_y(0.20 * inch, direction="d") - label.draw_text_cell(canvas, "PROMPT", font_name, 10, vertical_align='t', - inset_y=5., inset_x=0.) + label.draw_text_cell( + canvas, "PROMPT", font_name, 10, vertical_align="t", inset_y=5.0, inset_x=0.0 + ) - style = getSampleStyleSheet()['BodyText'] + style = getSampleStyleSheet()["BodyText"] style.fontName = font_name style.fontSize = 14 @@ -124,16 +157,17 @@ def draw_prompt(canvas, rect, prompt=""): block.draw_flowable(canvas, p, draw_baselines=True) - rect.draw_border(canvas, 'max_y') + rect.draw_border(canvas, "max_y") def draw_notes(canvas, rect, note=""): - label, block = rect.split_y(0.20 * inch, direction='d') + label, block = rect.split_y(0.20 * inch, direction="d") - label.draw_text_cell(canvas, "NOTES", font_name, 10, vertical_align='t', - inset_y=5., inset_x=0.) + label.draw_text_cell( + canvas, "NOTES", font_name, 10, vertical_align="t", inset_y=5.0, inset_x=0.0 + ) - style = getSampleStyleSheet()['BodyText'] + style = getSampleStyleSheet()["BodyText"] style.fontName = font_name style.fontSize = 14 style.leading = 24 @@ -142,7 +176,7 @@ def draw_notes(canvas, rect, note=""): block.draw_flowable(canvas, prompt, draw_baselines=True) - rect.draw_border(canvas, ['max_y', 'min_y']) + rect.draw_border(canvas, ["max_y", "min_y"]) def draw_take_grid(canvas, rect): @@ -178,74 +212,90 @@ def draw_take_grid(canvas, rect): ln.lineTo(rect.max_x, rect.min_y + y) canvas.drawPath(ln) - rect.draw_border(canvas, 'max_x') + rect.draw_border(canvas, "max_x") canvas.restoreState() -def draw_aux_block(canvas, rect, recording_time_sec_this_line, - recording_time_sec): - rect.draw_border(canvas, 'min_x') +def draw_aux_block(canvas, rect, recording_time_sec_this_line, recording_time_sec): + rect.draw_border(canvas, "min_x") - content_rect = rect.inset_xy(10., 10.) - lines, last_line = content_rect.divide_y([12., 12., 24., 24., 24., 24.], - direction='d') + content_rect = rect.inset_xy(10.0, 10.0) + lines, _ = content_rect.divide_y( + [12.0, 12.0, 24.0, 24.0, 24.0, 24.0], direction="d" + ) - lines[0].draw_text_cell(canvas, - "Time for this line: %.1f mins" % - (recording_time_sec_this_line / 60.), - font_name, 9.) - lines[1].draw_text_cell(canvas, "Running time: %03.1f mins" % - (recording_time_sec / 60.), font_name, 9.) - lines[2].draw_text_cell(canvas, "Actual Start: ______________", - font_name, 9., vertical_align='b') - lines[3].draw_text_cell(canvas, "Record Date: ______________", - font_name, 9., vertical_align='b') - lines[4].draw_text_cell(canvas, "Engineer: ______________", - font_name, 9., vertical_align='b') - lines[5].draw_text_cell(canvas, "Location: ______________", - font_name, 9., vertical_align='b') + lines[0].draw_text_cell( + canvas, + "Time for this line: %.1f mins" % (recording_time_sec_this_line / 60.0), + font_name, + 9.0, + ) + lines[1].draw_text_cell( + canvas, + "Running time: %03.1f mins" % (recording_time_sec / 60.0), + font_name, + 9.0, + ) + lines[2].draw_text_cell( + canvas, "Actual Start: ______________", font_name, 9.0, vertical_align="b" + ) + lines[3].draw_text_cell( + canvas, "Record Date: ______________", font_name, 9.0, vertical_align="b" + ) + lines[4].draw_text_cell( + canvas, "Engineer: ______________", font_name, 9.0, vertical_align="b" + ) + lines[5].draw_text_cell( + canvas, "Location: ______________", font_name, 9.0, vertical_align="b" + ) -def draw_footer(canvas, rect, record: ADRLine, report_date, line_no, - total_lines): - rect.draw_border(canvas, 'max_y') +def draw_footer(canvas, rect, record: ADRLine, report_date, line_no, total_lines): + rect.draw_border(canvas, "max_y") report_date_s = [report_date.strftime("%c")] spotting_name = [record.spot] if record.spot is not None else [] - pages_s = ["Line %i of %i" % (line_no, total_lines)] + pages_s = [f"Line {line_no} of {total_lines}"] footer_s = " - ".join(report_date_s + spotting_name + pages_s) - rect.draw_text_cell(canvas, footer_s, font_name=font_name, font_size=10., - inset_y=2.) + rect.draw_text_cell( + canvas, footer_s, font_name=font_name, font_size=10.0, inset_y=2.0 + ) -def create_report_for_character(records, report_date, - tc_display_format: TimecodeFormat): +def create_report_for_character( + records, report_date, tc_display_format: TimecodeFormat +): + + outfile = f"{records[0].title}_{records[0].character_id}_{records[0].character_name}_Log.pdf" - outfile = "%s_%s_%s_Log.pdf" % (records[0].title, - records[0].character_id, - records[0].character_name,) assert outfile is not None - assert outfile[-4:] == '.pdf', "Output file must have 'pdf' extension!" + assert outfile[-4:] == ".pdf", "Output file must have 'pdf' extension!" # pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) page: GRect = GRect(0, 0, letter[0], letter[1]) page = page.inset(inch * 0.5) - (header_row, char_row, data_row, - prompt_row, notes_row, takes_row), footer = \ - page.divide_y([0.875 * inch, 0.375 * inch, inch, - 3.0 * inch, 1.5 * inch, 3 * inch], direction='d') + (header_row, char_row, data_row, prompt_row, notes_row, takes_row), footer = ( + page.divide_y( + [0.875 * inch, 0.375 * inch, inch, 3.0 * inch, 1.5 * inch, 3 * inch], + direction="d", + ) + ) - cue_header_block, title_header_block = header_row.split_x(4.0 * inch) - (cue_number_block, timecode_block), reason_block = \ - data_row.divide_x([1.5 * inch, 1.5 * inch]) + cue_header_block, _title_header_block = header_row.split_x(4.0 * inch) + (cue_number_block, timecode_block), reason_block = data_row.divide_x( + [1.5 * inch, 1.5 * inch] + ) (take_grid_block), aux_block = takes_row.split_x(5.25 * inch) - c = Canvas(outfile, pagesize=letter,) + c = Canvas( + outfile, + pagesize=letter, + ) - c.setTitle("%s %s (%s) Supervisor's Log" % (records[0].title, - records[0].character_name, - records[0].character_id)) + c.setTitle( + f"{records[0].title} {records[0].character_name} ({records[0].character_id}) Supervisor's Log" + ) c.setAuthor(records[0].supervisor) recording_time_sec = 0.0 @@ -253,8 +303,7 @@ def create_report_for_character(records, report_date, line_n = 1 for record in records: record: ADRLine - recording_time_sec_this_line: float = ( - record.time_budget_mins or 6.0) * 60.0 + recording_time_sec_this_line: float = (record.time_budget_mins or 6.0) * 60.0 recording_time_sec = recording_time_sec + recording_time_sec_this_line draw_header_block(c, cue_header_block, record) @@ -264,17 +313,18 @@ def create_report_for_character(records, report_date, # draw_title_box(c, title_header_block, record) draw_character_row(c, char_row, record) draw_cue_number_block(c, cue_number_block, record) - draw_timecode_block(c, timecode_block, record, - tc_display_format=tc_display_format) + draw_timecode_block( + c, timecode_block, record, tc_display_format=tc_display_format + ) draw_reason_block(c, reason_block, record) draw_prompt(c, prompt_row, prompt=record.prompt or "") draw_notes(c, notes_row, note="") draw_take_grid(c, take_grid_block) - draw_aux_block(c, aux_block, recording_time_sec_this_line, - recording_time_sec) + draw_aux_block(c, aux_block, recording_time_sec_this_line, recording_time_sec) - draw_footer(c, footer, record, report_date, line_no=line_n, - total_lines=total_lines) + draw_footer( + c, footer, record, report_date, line_no=line_n, total_lines=total_lines + ) line_n = line_n + 1 c.showPage() @@ -285,9 +335,11 @@ def create_report_for_character(records, report_date, def output_report(lines, tc_display_format: TimecodeFormat): report_date = datetime.datetime.now() events = sorted(lines, key=lambda x: x.start) - character_numbers = set([x.character_id for x in lines]) + character_numbers = {x.character_id for x in lines} for n in character_numbers: - create_report_for_character([e for e in events if e.character_id == n], - report_date, - tc_display_format=tc_display_format) + create_report_for_character( + [e for e in events if e.character_id == n], + report_date, + tc_display_format=tc_display_format, + ) diff --git a/src/ptulsconv/pdf/talent_sides.py b/src/ptulsconv/pdf/talent_sides.py index 23f9c17..e61370a 100644 --- a/src/ptulsconv/pdf/talent_sides.py +++ b/src/ptulsconv/pdf/talent_sides.py @@ -1,82 +1,86 @@ -# -*- coding: utf-8 -*- -from typing import List - -from .__init__ import make_doc_template -from reportlab.lib.units import inch -from reportlab.lib.pagesizes import letter - -from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table, \ - HRFlowable -from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib import colors +from reportlab.lib.pagesizes import letter +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.units import inch +from reportlab.platypus import HRFlowable, KeepTogether, Paragraph, Spacer, Table # from reportlab.pdfbase import pdfmetrics # from reportlab.pdfbase.ttfonts import TTFont - from ..broadcast_timecode import TimecodeFormat from ..docparser.adr_entity import ADRLine +from .__init__ import make_doc_template -def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat, - font_name="Helvetica"): - character_numbers = set([n.character_id for n in lines]) +def output_report( + lines: list[ADRLine], tc_display_format: TimecodeFormat, font_name="Helvetica" +): + character_numbers = {n.character_id for n in lines} # pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) for n in character_numbers: - char_lines = [line for line in lines - if not line.omitted and line.character_id == n] + char_lines = [ + line for line in lines if not line.omitted and line.character_id == n + ] character_name = char_lines[0].character_name char_lines = sorted(char_lines, key=lambda line: line.start) - title = "%s (%s) %s ADR Script" % (char_lines[0].title, - character_name, n) - filename = "%s_%s_%s_ADR Script.pdf" % (char_lines[0].title, - n, character_name) + title = f"{char_lines[0].title} ({character_name}) {n} ADR Script" + filename = f"{char_lines[0].title}_{n}_{character_name}_ADR Script.pdf" - doc = make_doc_template(page_size=letter, filename=filename, - document_title=title, - title=char_lines[0].title, - document_subheader=char_lines[0].spot or "", - supervisor=char_lines[0].supervisor or "", - client=char_lines[0].client or "", - document_header=character_name or "") + doc = make_doc_template( + page_size=letter, + filename=filename, + document_title=title, + title=char_lines[0].title, + document_subheader=char_lines[0].spot or "", + supervisor=char_lines[0].supervisor or "", + client=char_lines[0].client or "", + document_header=character_name or "", + ) story = [] - prompt_style = getSampleStyleSheet()['Normal'] + prompt_style = getSampleStyleSheet()["Normal"] prompt_style.fontName = font_name - prompt_style.fontSize = 18. + prompt_style.fontSize = 18.0 - prompt_style.leading = 24. + prompt_style.leading = 24.0 prompt_style.leftIndent = 1.5 * inch prompt_style.rightIndent = 1.5 * inch - number_style = getSampleStyleSheet()['Normal'] + number_style = getSampleStyleSheet()["Normal"] number_style.fontName = font_name number_style.fontSize = 14 number_style.leading = 24 - number_style.leftIndent = 0. - number_style.rightIndent = 0. + number_style.leftIndent = 0.0 + number_style.rightIndent = 0.0 for line in char_lines: start_tc = tc_display_format.seconds_to_smpte(line.start) finish_tc = tc_display_format.seconds_to_smpte(line.finish) - data_block = [[Paragraph(line.cue_number, number_style), - Paragraph(start_tc + " - " + finish_tc, - number_style) - ]] + data_block = [ + [ + Paragraph(line.cue_number or "[No QN]", number_style), + Paragraph(start_tc + " - " + finish_tc, number_style), + ] + ] -# RIGHTWARDS ARROW → -# Unicode: U+2192, UTF-8: E2 86 92 + # RIGHTWARDS ARROW → + # Unicode: U+2192, UTF-8: E2 86 92 story.append( KeepTogether( - [HRFlowable(width='50%', color=colors.black), - Table(data=data_block, colWidths=[1.5 * inch, 6. * inch], - style=[('LEFTPADDING', (0, 0), (-1, -1), 0.)]), - Paragraph(line.prompt, prompt_style), - Spacer(1., inch * 1.5)] + [ + HRFlowable(width="50%", color=colors.black), + Table( + data=data_block, + colWidths=[1.5 * inch, 6.0 * inch], + style=[("LEFTPADDING", (0, 0), (-1, -1), 0.0)], + ), + Paragraph(line.prompt or "[No Prompt]", prompt_style), + Spacer(1.0, inch * 1.5), + ] ) ) diff --git a/src/ptulsconv/reporting.py b/src/ptulsconv/reporting.py index f861d2a..c96506b 100644 --- a/src/ptulsconv/reporting.py +++ b/src/ptulsconv/reporting.py @@ -9,63 +9,63 @@ import sys def print_banner_style(message): if sys.stderr.isatty(): - sys.stderr.write("\n\033[1m%s\033[0m\n\n" % message) + sys.stderr.write(f"\n\033[1m{message}\033[0m\n\n") else: - sys.stderr.write("\n%s\n\n" % message) + sys.stderr.write(f"\n{message}\n\n") def print_section_header_style(message): if sys.stderr.isatty(): - sys.stderr.write("\n\033[4m%s\033[0m\n\n" % message) + sys.stderr.write(f"\n\033[4m{message}\033[0m\n\n") else: - sys.stderr.write("%s\n\n" % message) + sys.stderr.write(f"{message}\n\n") def print_status_style(message): if sys.stderr.isatty(): - sys.stderr.write("\033[3m - %s\033[0m\n" % message) + sys.stderr.write(f"\033[3m - {message}\033[0m\n") else: - sys.stderr.write(" - %s\n" % message) + sys.stderr.write(f" - {message}\n") def print_warning(warning_string): if sys.stderr.isatty(): - sys.stderr.write("\033[3m - %s\033[0m\n" % warning_string) + sys.stderr.write(f"\033[3m - {warning_string}\033[0m\n") else: - sys.stderr.write(" - %s\n" % warning_string) + sys.stderr.write(f" - {warning_string}\n") -def print_advisory_tagging_error(failed_string, position, - parent_track_name=None, clip_time=None): +def print_advisory_tagging_error( + failed_string, position, parent_track_name=None, clip_time=None +): if sys.stderr.isatty(): sys.stderr.write("\n") sys.stderr.write(" ! \033[33;1mTagging error: \033[0m") ok_string = failed_string[:position] not_ok_string = failed_string[position:] - sys.stderr.write("\033[32m\"%s\033[31;1m%s\"\033[0m\n" % - (ok_string, not_ok_string)) + sys.stderr.write(f'\033[32m"{ok_string}\033[31;1m{not_ok_string}"\033[0m\n') if parent_track_name is not None: - sys.stderr.write(" ! > On track \"%s\"\n" % parent_track_name) + sys.stderr.write(f' ! > On track "{parent_track_name}"\n') if clip_time is not None: - sys.stderr.write(" ! > In clip name at %s\n" % clip_time) + sys.stderr.write(f" ! > In clip name at {clip_time}\n") else: sys.stderr.write("\n") - sys.stderr.write(" ! Tagging error: \"%s\"\n" % failed_string) + sys.stderr.write(f' ! Tagging error: "{failed_string}"\n') sys.stderr.write(" ! %s _______________⬆\n" % (" " * position)) if parent_track_name is not None: - sys.stderr.write(" ! > On track \"%s\"\n" % parent_track_name) + sys.stderr.write(f' ! > On track "{parent_track_name}"\n') if clip_time is not None: - sys.stderr.write(" ! > In clip name at %s\n" % clip_time) + sys.stderr.write(f" ! > In clip name at {clip_time}\n") sys.stderr.write("\n") def print_fatal_error(message): if sys.stderr.isatty(): - sys.stderr.write("\n\033[5;31;1m*** %s ***\033[0m\n" % message) + sys.stderr.write(f"\n\033[5;31;1m*** {message} ***\033[0m\n") else: - sys.stderr.write("\n%s\n" % message) + sys.stderr.write(f"\n{message}\n") diff --git a/src/ptulsconv/validations.py b/src/ptulsconv/validations.py index ded4498..e7890f1 100644 --- a/src/ptulsconv/validations.py +++ b/src/ptulsconv/validations.py @@ -2,31 +2,34 @@ Validation logic for enforcing various consistency rules. """ +from __future__ import annotations + +from collections.abc import Iterator from dataclasses import dataclass + from ptulsconv.docparser.adr_entity import ADRLine -from typing import Iterator, Optional @dataclass class ValidationError: message: str - event: Optional[ADRLine] = None + event: ADRLine | None = None def report_message(self): if self.event is not None: - return (f"{self.message}: event at {self.event.start} with number" - "{self.event.cue_number}") + return ( + f"{self.message}: event at {self.event.start} with number" + "{self.event.cue_number}" + ) else: return self.message -def validate_unique_count(input_lines: Iterator[ADRLine], field='title', - count=1): - values = set(list(map(lambda e: getattr(e, field), input_lines))) +def validate_unique_count(input_lines: Iterator[ADRLine], field="title", count=1): + values = {getattr(e, field) for e in input_lines} if len(values) > count: yield ValidationError( - message="Field {} has too many values (max={}): {}" - .format(field, count, values) + message=f"Field {field} has too many values (max={count}): {values}" ) @@ -34,52 +37,52 @@ def validate_value(input_lines: Iterator[ADRLine], key_field, predicate): for event in input_lines: val = getattr(event, key_field) if not predicate(val): - yield ValidationError(message='Field {} not in range'.format(val), - event=event) + yield ValidationError(message=f"Field {val} not in range", event=event) -def validate_unique_field(input_lines: Iterator[ADRLine], field='cue_number', - scope=None): - values = dict() +def validate_unique_field( + input_lines: Iterator[ADRLine], field="cue_number", scope=None +): + values = {} for event in input_lines: this = getattr(event, field) if scope is not None: key = getattr(event, scope) else: - key = '_values' + key = "_values" values.setdefault(key, set()) if this in values[key]: - yield ValidationError(message='Re-used {}'.format(field), - event=event) + yield ValidationError(message=f"Re-used {field}", event=event) else: values[key].update(this) -def validate_non_empty_field(input_lines: Iterator[ADRLine], - field='cue_number'): +def validate_non_empty_field(input_lines: Iterator[ADRLine], field="cue_number"): for event in input_lines: if getattr(event, field, None) is None: - yield ValidationError(message='Empty field {}'.format(field), - event=event) + yield ValidationError(message=f"Empty field {field}", event=event) -def validate_dependent_value(input_lines: Iterator[ADRLine], key_field, - dependent_field): +def validate_dependent_value( + input_lines: Iterator[ADRLine], key_field, dependent_field +): """ Validates that two events with the same value in `key_field` always have the same value in `dependent_field` """ - key_values = set((getattr(x, key_field) for x in input_lines)) + key_values = {getattr(x, key_field) for x in input_lines} for key_value in key_values: - rows = [(getattr(x, key_field), getattr(x, dependent_field)) - for x in input_lines - if getattr(x, key_field) == key_value] + rows = [ + (getattr(x, key_field), getattr(x, dependent_field)) + for x in input_lines + if getattr(x, key_field) == key_value + ] unique_rows = set(rows) if len(unique_rows) > 1: - message = "Non-unique values for key {} = ".format(key_field) + message = f"Non-unique values for key {key_field} = " for u in unique_rows: - message = message + "\n - {} -> {}".format(u[0], u[1]) + message = message + f"\n - {u[0]} -> {u[1]}" yield ValidationError(message=message, event=None) diff --git a/src/ptulsconv/xml/common.py b/src/ptulsconv/xml/common.py index 3554c87..85382f0 100644 --- a/src/ptulsconv/xml/common.py +++ b/src/ptulsconv/xml/common.py @@ -1,12 +1,14 @@ +from __future__ import annotations + +import glob import os import os.path import pathlib import subprocess import sys -import glob -import datetime +from datetime import datetime +from importlib.metadata import version as module_version from xml.etree.ElementTree import TreeBuilder, tostring -from typing import List import ptulsconv from ptulsconv.docparser.adr_entity import ADRLine @@ -14,138 +16,160 @@ from ptulsconv.docparser.adr_entity import ADRLine # TODO Get a third-party test for Avid Marker lists -def avid_marker_list(lines: List[ADRLine], report_date=datetime.datetime.now(), - reel_start_frame=0, fps=24): +def avid_marker_list( + lines: list[ADRLine], + report_date: datetime | None = None, + reel_start_frame=0, + fps=24, +): doc = TreeBuilder(element_factory=None) - doc.start('Avid:StreamItems', {'xmlns:Avid': 'http://www.avid.com'}) - doc.start('Avid:XMLFileData', {}) - doc.start('AvProp', {'name': 'DomainMagic', 'type': 'string'}) + if report_date is None: + report_date = datetime.now() + + doc.start("Avid:StreamItems", {"xmlns:Avid": "http://www.avid.com"}) + doc.start("Avid:XMLFileData", {}) + doc.start("AvProp", {"name": "DomainMagic", "type": "string"}) doc.data("Domain") - doc.end('AvProp') - doc.start('AvProp', {'name': 'DomainKey', 'type': 'string'}) + doc.end("AvProp") + doc.start("AvProp", {"name": "DomainKey", "type": "string"}) doc.data("58424a44") - doc.end('AvProp') + doc.end("AvProp") def insert_elem(kind, attb, atype, name, value): - doc.start('ListElem', {}) - doc.start('AvProp', {'id': 'ATTR', - 'name': 'OMFI:ATTB:Kind', - 'type': 'int32'}) + doc.start("ListElem", {}) + doc.start("AvProp", {"id": "ATTR", "name": "OMFI:ATTB:Kind", "type": "int32"}) doc.data(kind) - doc.end('AvProp') + doc.end("AvProp") - doc.start('AvProp', {'id': 'ATTR', - 'name': 'OMFI:ATTB:Name', - 'type': 'string'}) + doc.start("AvProp", {"id": "ATTR", "name": "OMFI:ATTB:Name", "type": "string"}) doc.data(name) - doc.end('AvProp') + doc.end("AvProp") - doc.start('AvProp', {'id': 'ATTR', - 'name': attb, - 'type': atype}) + doc.start("AvProp", {"id": "ATTR", "name": attb, "type": atype}) doc.data(value) - doc.end('AvProp') + doc.end("AvProp") - doc.end('ListElem') + doc.end("ListElem") for line in lines: - doc.start('AvClass', {'id': 'ATTR'}) - doc.start('AvProp', {'id': 'ATTR', - 'name': '__OMFI:ATTR:NumItems', - 'type': 'int32'}) - doc.data('7') - doc.end('AvProp') + doc.start("AvClass", {"id": "ATTR"}) + doc.start( + "AvProp", {"id": "ATTR", "name": "__OMFI:ATTR:NumItems", "type": "int32"} + ) + doc.data("7") + doc.end("AvProp") - doc.start('List', {'id': 'OMFI:ATTR:AttrRefs'}) + doc.start("List", {"id": "OMFI:ATTR:AttrRefs"}) - insert_elem('1', 'OMFI:ATTB:IntAttribute', 'int32', - '_ATN_CRM_LONG_CREATE_DATE', report_date.strftime("%s")) - insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string', - '_ATN_CRM_COLOR', 'yellow') - insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string', - '_ATN_CRM_USER', line.supervisor or "") + assert report_date - marker_name = "%s: %s" % (line.cue_number, line.prompt) - insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string', - '_ATN_CRM_COM', marker_name) + insert_elem( + "1", + "OMFI:ATTB:IntAttribute", + "int32", + "_ATN_CRM_LONG_CREATE_DATE", + report_date.strftime("%s"), + ) + insert_elem( + "2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COLOR", "yellow" + ) + insert_elem( + "2", + "OMFI:ATTB:StringAttribute", + "string", + "_ATN_CRM_USER", + line.supervisor or "", + ) + + marker_name = f"{line.cue_number}: {line.prompt}" + insert_elem( + "2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COM", marker_name + ) start_frame = int(line.start * fps) - insert_elem('2', "OMFI:ATTB:StringAttribute", 'string', - '_ATN_CRM_TC', - str(start_frame - reel_start_frame)) + insert_elem( + "2", + "OMFI:ATTB:StringAttribute", + "string", + "_ATN_CRM_TC", + str(start_frame - reel_start_frame), + ) - insert_elem('2', "OMFI:ATTB:StringAttribute", 'string', - '_ATN_CRM_TRK', 'V1') - insert_elem('1', "OMFI:ATTB:IntAttribute", 'int32', - '_ATN_CRM_LENGTH', '1') + insert_elem("2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_TRK", "V1") + insert_elem("1", "OMFI:ATTB:IntAttribute", "int32", "_ATN_CRM_LENGTH", "1") - doc.start('ListElem', {}) - doc.end('ListElem') + doc.start("ListElem", {}) + doc.end("ListElem") - doc.end('List') - doc.end('AvClass') + doc.end("List") + doc.end("AvClass") - doc.end('Avid:XMLFileData') - doc.end('Avid:StreamItems') + doc.end("Avid:XMLFileData") + doc.end("Avid:StreamItems") def dump_fmpxml(data, input_file_name, output, adr_field_map): doc = TreeBuilder(element_factory=None) - doc.start('FMPXMLRESULT', {'xmlns': - 'http://www.filemaker.com/fmpxmlresult'}) + doc.start("FMPXMLRESULT", {"xmlns": "http://www.filemaker.com/fmpxmlresult"}) - doc.start('ERRORCODE', {}) - doc.data('0') - doc.end('ERRORCODE') + doc.start("ERRORCODE", {}) + doc.data("0") + doc.end("ERRORCODE") - doc.start('PRODUCT', {'NAME': ptulsconv.__name__, - 'VERSION': ptulsconv.__version__}) - doc.end('PRODUCT') + version = module_version("ptulsconv") + doc.start("PRODUCT", {"NAME": ptulsconv.__name__, "VERSION": f"{version}"}) + doc.end("PRODUCT") - doc.start('DATABASE', {'DATEFORMAT': 'MM/dd/yy', - 'LAYOUT': 'summary', - 'TIMEFORMAT': 'hh:mm:ss', - 'RECORDS': str(len(data['events'])), - 'NAME': os.path.basename(input_file_name)}) - doc.end('DATABASE') + doc.start( + "DATABASE", + { + "DATEFORMAT": "MM/dd/yy", + "LAYOUT": "summary", + "TIMEFORMAT": "hh:mm:ss", + "RECORDS": str(len(data["events"])), + "NAME": os.path.basename(input_file_name), + }, + ) + doc.end("DATABASE") - doc.start('METADATA', {}) + doc.start("METADATA", {}) for field in adr_field_map: tp = field[2] - ft = 'TEXT' + ft = "TEXT" if tp is int or tp is float: - ft = 'NUMBER' + ft = "NUMBER" - doc.start('FIELD', {'EMPTYOK': 'YES', 'MAXREPEAT': '1', - 'NAME': field[1], 'TYPE': ft}) - doc.end('FIELD') - doc.end('METADATA') + doc.start( + "FIELD", {"EMPTYOK": "YES", "MAXREPEAT": "1", "NAME": field[1], "TYPE": ft} + ) + doc.end("FIELD") + doc.end("METADATA") - doc.start('RESULTSET', {'FOUND': str(len(data['events']))}) - for event in data['events']: - doc.start('ROW', {}) + doc.start("RESULTSET", {"FOUND": str(len(data["events"]))}) + for event in data["events"]: + doc.start("ROW", {}) for field in adr_field_map: - doc.start('COL', {}) - doc.start('DATA', {}) + doc.start("COL", {}) + doc.start("DATA", {}) for key_attempt in field[0]: - if key_attempt in event.keys(): + if key_attempt in event: doc.data(str(event[key_attempt])) break - doc.end('DATA') - doc.end('COL') - doc.end('ROW') - doc.end('RESULTSET') + doc.end("DATA") + doc.end("COL") + doc.end("ROW") + doc.end("RESULTSET") - doc.end('FMPXMLRESULT') + doc.end("FMPXMLRESULT") docelem = doc.close() - xmlstr = tostring(docelem, encoding='unicode', method='xml') + xmlstr = tostring(docelem, encoding="unicode", method="xml") output.write(xmlstr) -xslt_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'xslt') +xslt_path = os.path.join(pathlib.Path(__file__).parent.absolute(), "xslt") def xform_options(): @@ -154,7 +178,7 @@ def xform_options(): def dump_xform_options(output=sys.stdout): print("# Available transforms:", file=output) - print("# Transform dir: %s" % xslt_path, file=output) + print(f"# Transform dir: {xslt_path}", file=output) for f in xform_options(): base = os.path.basename(f) name, _ = os.path.splitext(base) @@ -162,22 +186,29 @@ def dump_xform_options(output=sys.stdout): def fmp_transformed_dump(data, input_file, xsl_name, output, adr_field_map): - from ptulsconv.reporting import print_status_style import io + from ptulsconv.reporting import print_status_style + pipe = io.StringIO() print_status_style("Generating base XML") dump_fmpxml(data, input_file, pipe, adr_field_map) str_data = pipe.getvalue() - print_status_style("Base XML size %i" % (len(str_data))) + print_status_style(f"Base XML size {len(str_data)}") print_status_style("Running xsltproc") - xsl_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'xslt', - xsl_name + ".xsl") - print_status_style("Using xsl: %s" % xsl_path) - subprocess.run(['xsltproc', xsl_path, '-'], - input=str_data, text=True, - stdout=output, shell=False, check=True) + xsl_path = os.path.join( + pathlib.Path(__file__).parent.absolute(), "xslt", xsl_name + ".xsl" + ) + print_status_style(f"Using xsl: {xsl_path}") + subprocess.run( + ["xsltproc", xsl_path, "-"], + input=str_data, + text=True, + stdout=output, + shell=False, + check=True, + ) diff --git a/tests/export_tests/test_robinhood1.py b/tests/export_tests/test_robinhood1.py index 6901d39..e2614d0 100644 --- a/tests/export_tests/test_robinhood1.py +++ b/tests/export_tests/test_robinhood1.py @@ -1,25 +1,26 @@ -import unittest -from ptulsconv.docparser import parse_document import os.path +import unittest + +from ptulsconv.docparser import parse_document class TestRobinHood1(unittest.TestCase): - path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting.txt' + path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting.txt" def test_header_export(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) self.assertIsNotNone(session.header) - self.assertEqual(session.header.session_name, 'Robin Hood Spotting') + self.assertEqual(session.header.session_name, "Robin Hood Spotting") self.assertEqual(session.header.sample_rate, 48000.0) self.assertEqual(session.header.bit_depth, 24) - self.assertEqual(session.header.timecode_fps, '29.97') + self.assertEqual(session.header.timecode_fps, "29.97") self.assertEqual(session.header.timecode_drop_frame, False) def test_all_sections(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) self.assertIsNotNone(session.header) @@ -31,52 +32,74 @@ class TestRobinHood1(unittest.TestCase): def test_tracks(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) self.assertEqual(len(session.tracks), 14) - self.assertListEqual(["Scenes", "Robin", "Will", "Marian", "John", - "Guy", "Much", "Butcher", "Town Crier", - "Soldier 1", "Soldier 2", "Soldier 3", - "Priest", "Guest at Court"], - list(map(lambda t: t.name, session.tracks))) - self.assertListEqual(["", "[ADR] {Actor=Errol Flynn} $CN=1", - "[ADR] {Actor=Patrick Knowles} $CN=2", - "[ADR] {Actor=Olivia DeHavilland} $CN=3", - "[ADR] {Actor=Claude Raines} $CN=4", - "[ADR] {Actor=Basil Rathbone} $CN=5", - "[ADR] {Actor=Herbert Mundin} $CN=6", - "[ADR] {Actor=George Bunny} $CN=101", - "[ADR] {Actor=Leonard Mundie} $CN=102", - "[ADR] $CN=103", - "[ADR] $CN=104", - "[ADR] $CN=105", - "[ADR] {Actor=Thomas R. Mills} $CN=106", - "[ADR] $CN=107"], - list(map(lambda t: t.comments, session.tracks))) + self.assertListEqual( + [ + "Scenes", + "Robin", + "Will", + "Marian", + "John", + "Guy", + "Much", + "Butcher", + "Town Crier", + "Soldier 1", + "Soldier 2", + "Soldier 3", + "Priest", + "Guest at Court", + ], + list(map(lambda t: t.name, session.tracks)), + ) + self.assertListEqual( + [ + "", + "[ADR] {Actor=Errol Flynn} $CN=1", + "[ADR] {Actor=Patrick Knowles} $CN=2", + "[ADR] {Actor=Olivia DeHavilland} $CN=3", + "[ADR] {Actor=Claude Raines} $CN=4", + "[ADR] {Actor=Basil Rathbone} $CN=5", + "[ADR] {Actor=Herbert Mundin} $CN=6", + "[ADR] {Actor=George Bunny} $CN=101", + "[ADR] {Actor=Leonard Mundie} $CN=102", + "[ADR] $CN=103", + "[ADR] $CN=104", + "[ADR] $CN=105", + "[ADR] {Actor=Thomas R. Mills} $CN=106", + "[ADR] $CN=107", + ], + list(map(lambda t: t.comments, session.tracks)), + ) def test_a_track(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) - + guy_track = session.tracks[5] - self.assertEqual(guy_track.name, 'Guy') - self.assertEqual(guy_track.comments, '[ADR] {Actor=Basil Rathbone} $CN=5') + self.assertEqual(guy_track.name, "Guy") + self.assertEqual(guy_track.comments, "[ADR] {Actor=Basil Rathbone} $CN=5") self.assertEqual(guy_track.user_delay_samples, 0) self.assertListEqual(guy_track.state, []) self.assertEqual(len(guy_track.clips), 16) self.assertEqual(guy_track.clips[5].channel, 1) self.assertEqual(guy_track.clips[5].event, 6) - self.assertEqual(guy_track.clips[5].clip_name, "\"What's your name? You Saxon dog!\" $QN=GY106") + self.assertEqual( + guy_track.clips[5].clip_name, + '"What\'s your name? You Saxon dog!" $QN=GY106', + ) self.assertEqual(guy_track.clips[5].start_timecode, "01:04:19:15") self.assertEqual(guy_track.clips[5].finish_timecode, "01:04:21:28") self.assertEqual(guy_track.clips[5].duration, "00:00:02:13") self.assertEqual(guy_track.clips[5].timestamp, None) - self.assertEqual(guy_track.clips[5].state, 'Unmuted') + self.assertEqual(guy_track.clips[5].state, "Unmuted") def test_memory_locations(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) self.assertEqual(len(session.markers), 1) @@ -86,5 +109,5 @@ class TestRobinHood1(unittest.TestCase): self.assertEqual(session.markers[0].units, "Samples") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/export_tests/test_robinhood5.py b/tests/export_tests/test_robinhood5.py index 54c5f11..377bd42 100644 --- a/tests/export_tests/test_robinhood5.py +++ b/tests/export_tests/test_robinhood5.py @@ -1,52 +1,56 @@ -import unittest -from ptulsconv.docparser import parse_document import os.path +import unittest + +from ptulsconv.docparser import parse_document class TestRobinHood5(unittest.TestCase): - path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting5.txt' + path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting5.txt" def test_skipped_segments(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) self.assertIsNone(session.files) self.assertIsNone(session.clips) def test_plugins(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) self.assertEqual(len(session.plugins), 2) def test_stereo_track(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) - self.assertEqual(session.tracks[1].name, 'MX WT (Stereo)') + self.assertEqual(session.tracks[1].name, "MX WT (Stereo)") self.assertEqual(len(session.tracks[1].clips), 2) - self.assertEqual(session.tracks[1].clips[0].clip_name, 'RobinHood.1-01.L') - self.assertEqual(session.tracks[1].clips[1].clip_name, 'RobinHood.1-01.R') + self.assertEqual(session.tracks[1].clips[0].clip_name, "RobinHood.1-01.L") + self.assertEqual(session.tracks[1].clips[1].clip_name, "RobinHood.1-01.R") def test_a_track(self): - with open(self.path,"r") as file: + with open(self.path, "r") as file: session = parse_document(file.read()) guy_track = session.tracks[8] - self.assertEqual(guy_track.name, 'Guy') - self.assertEqual(guy_track.comments, '[ADR] {Actor=Basil Rathbone} $CN=5') + self.assertEqual(guy_track.name, "Guy") + self.assertEqual(guy_track.comments, "[ADR] {Actor=Basil Rathbone} $CN=5") self.assertEqual(guy_track.user_delay_samples, 0) - self.assertListEqual(guy_track.state, ['Solo']) + self.assertListEqual(guy_track.state, ["Solo"]) self.assertEqual(len(guy_track.clips), 16) self.assertEqual(guy_track.clips[5].channel, 1) self.assertEqual(guy_track.clips[5].event, 6) - self.assertEqual(guy_track.clips[5].clip_name, "\"What's your name? You Saxon dog!\" $QN=GY106") + self.assertEqual( + guy_track.clips[5].clip_name, + '"What\'s your name? You Saxon dog!" $QN=GY106', + ) self.assertEqual(guy_track.clips[5].start_timecode, "01:04:19:15.00") self.assertEqual(guy_track.clips[5].finish_timecode, "01:04:21:28.00") self.assertEqual(guy_track.clips[5].duration, "00:00:02:13.00") self.assertEqual(guy_track.clips[5].timestamp, "01:04:19:09.70") - self.assertEqual(guy_track.clips[5].state, 'Unmuted') + self.assertEqual(guy_track.clips[5].state, "Unmuted") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/export_tests/test_robinhood6.py b/tests/export_tests/test_robinhood6.py index c32440a..04627f3 100644 --- a/tests/export_tests/test_robinhood6.py +++ b/tests/export_tests/test_robinhood6.py @@ -1,33 +1,39 @@ -import unittest -from ptulsconv.docparser import parse_document import os.path +import unittest + +from ptulsconv.docparser import parse_document class TestRobinHood6(unittest.TestCase): - path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting6.txt' + path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting6.txt" def test_a_track(self): with open(self.path, "r") as file: session = parse_document(file.read()) - marian_track = session.tracks[6] - self.assertEqual(marian_track.name, 'Marian') - self.assertEqual(marian_track.comments, '[ADR] {Actor=Olivia DeHavilland} $CN=3') + self.assertEqual(marian_track.name, "Marian") + self.assertEqual( + marian_track.comments, "[ADR] {Actor=Olivia DeHavilland} $CN=3" + ) self.assertEqual(marian_track.user_delay_samples, 0) - self.assertListEqual(marian_track.state, ['Solo']) + self.assertListEqual(marian_track.state, ["Solo"]) self.assertEqual(len(marian_track.clips), 4) - self.assertListEqual(marian_track.plugins, ['Channel Strip (mono)', 'ReVibe II (mono/5.1)']) + self.assertListEqual( + marian_track.plugins, ["Channel Strip (mono)", "ReVibe II (mono/5.1)"] + ) self.assertEqual(marian_track.clips[2].channel, 1) self.assertEqual(marian_track.clips[2].event, 3) - self.assertEqual(marian_track.clips[2].clip_name, - "\"Isn't that reason enough for a Royal Ward who must obey her guardian?\" $QN=M103") + self.assertEqual( + marian_track.clips[2].clip_name, + '"Isn\'t that reason enough for a Royal Ward who must obey her guardian?" $QN=M103', + ) self.assertEqual(marian_track.clips[2].start_timecode, "01:08:01:11") self.assertEqual(marian_track.clips[2].finish_timecode, "01:08:04:24") self.assertEqual(marian_track.clips[2].duration, "00:00:03:12") self.assertEqual(marian_track.clips[2].timestamp, "01:08:01:11") - self.assertEqual(marian_track.clips[2].state, 'Unmuted') + self.assertEqual(marian_track.clips[2].state, "Unmuted") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/export_tests/test_robinhooddf.py b/tests/export_tests/test_robinhooddf.py index b9ead7b..fa5d2d1 100644 --- a/tests/export_tests/test_robinhooddf.py +++ b/tests/export_tests/test_robinhooddf.py @@ -1,16 +1,17 @@ -import unittest -from ptulsconv.docparser import parse_document import os.path +import unittest + +from ptulsconv.docparser import parse_document class TestRobinHoodDF(unittest.TestCase): - path = os.path.dirname(__file__) + '/../export_cases/Robin Hood SpottingDF.txt' + path = os.path.dirname(__file__) + "/../export_cases/Robin Hood SpottingDF.txt" def test_header_export_df(self): - + with open(self.path, "r") as file: session = parse_document(file.read()) - + self.assertEqual(session.header.timecode_drop_frame, True) def test_a_track(self): @@ -19,16 +20,18 @@ class TestRobinHoodDF(unittest.TestCase): session = parse_document(file.read()) guy_track = session.tracks[4] - self.assertEqual(guy_track.name, 'Robin') - self.assertEqual(guy_track.comments, '[ADR] {Actor=Errol Flynn} $CN=1') + self.assertEqual(guy_track.name, "Robin") + self.assertEqual(guy_track.comments, "[ADR] {Actor=Errol Flynn} $CN=1") self.assertEqual(guy_track.user_delay_samples, 0) self.assertListEqual(guy_track.state, []) self.assertEqual(len(guy_track.clips), 10) self.assertEqual(guy_track.clips[5].channel, 1) self.assertEqual(guy_track.clips[5].event, 6) - self.assertEqual(guy_track.clips[5].clip_name, "\"Hold there! What's his fault?\" $QN=R106") + self.assertEqual( + guy_track.clips[5].clip_name, '"Hold there! What\'s his fault?" $QN=R106' + ) self.assertEqual(guy_track.clips[5].start_timecode, "01:05:30;15") self.assertEqual(guy_track.clips[5].finish_timecode, "01:05:32;01") self.assertEqual(guy_track.clips[5].duration, "00:00:01;16") self.assertEqual(guy_track.clips[5].timestamp, None) - self.assertEqual(guy_track.clips[5].state, 'Unmuted') + self.assertEqual(guy_track.clips[5].state, "Unmuted") diff --git a/tests/functional/test_pdf_export.py b/tests/functional/test_pdf_export.py index ffca859..751e55d 100644 --- a/tests/functional/test_pdf_export.py +++ b/tests/functional/test_pdf_export.py @@ -1,28 +1,24 @@ +import os +import os.path +import tempfile import unittest -import tempfile - -import sys -import os.path -import os -import glob from ptulsconv import commands class TestPDFExport(unittest.TestCase): def test_report_generation(self): """ - Setp through every text file in export_cases and make sure it can + Setp through every text file in export_cases and make sure it can be converted into PDF docs without throwing an error """ files = [] - files = [os.path.dirname(__file__) + - "/../export_cases/Robin Hood Spotting.txt"] + files = [os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting.txt"] for path in files: tempdir = tempfile.TemporaryDirectory() os.chdir(tempdir.name) try: - commands.convert(input_file=path, major_mode='doc') + commands.convert(input_file=path, major_mode="doc") except Exception as e: print("Error in test_report_generation") print(f"File: {path}") @@ -33,13 +29,14 @@ class TestPDFExport(unittest.TestCase): def test_report_generation_track_markers(self): files = [] - files.append(os.path.dirname(__file__) + - "/../export_cases/Test for ptulsconv.txt") + files.append( + os.path.dirname(__file__) + "/../export_cases/Test for ptulsconv.txt" + ) for path in files: tempdir = tempfile.TemporaryDirectory() os.chdir(tempdir.name) try: - commands.convert(input_file=path, major_mode='doc') + commands.convert(input_file=path, major_mode="doc") except Exception as e: print("Error in test_report_generation_track_markers") print(f"File: {path}") @@ -49,5 +46,5 @@ class TestPDFExport(unittest.TestCase): tempdir.cleanup() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unittests/test_adr_entity.py b/tests/unittests/test_adr_entity.py index dd635c9..b78cc76 100644 --- a/tests/unittests/test_adr_entity.py +++ b/tests/unittests/test_adr_entity.py @@ -1,38 +1,41 @@ import unittest - -from ptulsconv.docparser.tag_compiler import Event -from ptulsconv.docparser.adr_entity import ADRLine, make_entity from fractions import Fraction +from ptulsconv.docparser.adr_entity import ADRLine, make_entity +from ptulsconv.docparser.tag_compiler import Event + class TestADREntity(unittest.TestCase): def test_event2line(self): tags = { - 'Ver': '1.0', - 'Actor': "Bill", - 'CN': "1", - 'QN': 'J1001', - 'R': 'Noise', - 'EFF': 'EFF' + "Ver": "1.0", + "Actor": "Bill", + "CN": "1", + "QN": "J1001", + "R": "Noise", + "EFF": "EFF", } - event = Event(clip_name='"This is a test." (sotto voce)', - track_name="Justin", - session_name="Test Project", - tags=tags, - start=Fraction(0, 1), finish=Fraction(1, 1)) + event = Event( + clip_name='"This is a test." (sotto voce)', + track_name="Justin", + session_name="Test Project", + tags=tags, + start=Fraction(0, 1), + finish=Fraction(1, 1), + ) line = make_entity(event) self.assertIsInstance(line, ADRLine) - self.assertEqual('Bill', line.actor_name) - self.assertEqual('Justin', line.character_name) + self.assertEqual("Bill", line.actor_name) + self.assertEqual("Justin", line.character_name) self.assertEqual('"This is a test." (sotto voce)', line.prompt) - self.assertEqual('Noise', line.reason) - self.assertEqual('J1001', line.cue_number) + self.assertEqual("Noise", line.reason) + self.assertEqual("J1001", line.cue_number) self.assertEqual(True, line.effort) - self.assertEqual('Test Project', line.title) - self.assertEqual('1.0', line.version) + self.assertEqual("Test Project", line.title) + self.assertEqual("1.0", line.version) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unittests/test_broadcast_timecode.py b/tests/unittests/test_broadcast_timecode.py index dbaa35d..60977a9 100644 --- a/tests/unittests/test_broadcast_timecode.py +++ b/tests/unittests/test_broadcast_timecode.py @@ -1,7 +1,9 @@ import unittest -from ptulsconv import broadcast_timecode from fractions import Fraction +from ptulsconv import broadcast_timecode + + class TestBroadcastTimecode(unittest.TestCase): def test_basic_to_frame_count(self): r1 = "01:00:00:00" @@ -66,7 +68,9 @@ class TestBroadcastTimecode(unittest.TestCase): def test_seconds_to_smpte(self): secs = Fraction(25, 24) frame_duration = Fraction(1, 24) - tc_format = broadcast_timecode.TimecodeFormat(frame_duration=frame_duration, logical_fps=24, drop_frame=False) + tc_format = broadcast_timecode.TimecodeFormat( + frame_duration=frame_duration, logical_fps=24, drop_frame=False + ) s1 = tc_format.seconds_to_smpte(secs) self.assertEqual(s1, "00:00:01:01") @@ -77,9 +81,11 @@ class TestBroadcastTimecode(unittest.TestCase): def test_unparseable_timecode(self): time_str = "11.32-19" - s1 = broadcast_timecode.smpte_to_frame_count(time_str, frames_per_logical_second=24) + s1 = broadcast_timecode.smpte_to_frame_count( + time_str, frames_per_logical_second=24 + ) self.assertIsNone(s1) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unittests/test_doc_entities.py b/tests/unittests/test_doc_entities.py index ea9b92c..f664747 100644 --- a/tests/unittests/test_doc_entities.py +++ b/tests/unittests/test_doc_entities.py @@ -1,24 +1,26 @@ import unittest -from ptulsconv.docparser.doc_entity import HeaderDescriptor from fractions import Fraction +from ptulsconv.docparser.doc_entity import HeaderDescriptor + class DocParserTestCase(unittest.TestCase): - def test_header(self): - header = HeaderDescriptor(session_name="Test Session", - sample_rate=48000.0, - bit_depth=24, - start_timecode="00:59:52:00", - timecode_format="30", - timecode_drop_frame=False, - count_audio_tracks=0, - count_clips=0, - count_files=0) + header = HeaderDescriptor( + session_name="Test Session", + sample_rate=48000.0, + bit_depth=24, + start_timecode="00:59:52:00", + timecode_format="30", + timecode_drop_frame=False, + count_audio_tracks=0, + count_clips=0, + count_files=0, + ) self.assertEqual(header.session_name, "Test Session") self.assertEqual(header.start_time, Fraction((59 * 60 + 52) * 30, 30)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unittests/test_footage.py b/tests/unittests/test_footage.py index a1bdb57..ca7a6ba 100644 --- a/tests/unittests/test_footage.py +++ b/tests/unittests/test_footage.py @@ -1,6 +1,8 @@ import unittest + from ptulsconv import footage + class TestFootage(unittest.TestCase): def test_basic_footage(self): r1 = "90+0" @@ -11,5 +13,3 @@ class TestFootage(unittest.TestCase): r1 = "1+8" f1 = footage.footage_to_seconds(r1) self.assertEqual(float(f1 or 0), 1.0) - - diff --git a/tests/unittests/test_tag_compiler.py b/tests/unittests/test_tag_compiler.py index 587a4ca..95f81dc 100644 --- a/tests/unittests/test_tag_compiler.py +++ b/tests/unittests/test_tag_compiler.py @@ -1,12 +1,11 @@ import unittest +from fractions import Fraction import ptulsconv.docparser.tag_compiler from ptulsconv.docparser import doc_entity -from fractions import Fraction class TestTagCompiler(unittest.TestCase): - def test_one_track(self): c = ptulsconv.docparser.tag_compiler.TagCompiler() @@ -16,31 +15,48 @@ class TestTagCompiler(unittest.TestCase): events = c.compile_events() event1 = next(events) - self.assertEqual('This is clip 1', event1.clip_name) - self.assertEqual('Track 1', event1.track_name) - self.assertEqual('Test Session', event1.session_name) - self.assertEqual(dict(A='A', - Color='Blue', - Ver='1.1', - Mode='2', - Comment='This is some text in the comments', - Part='1'), event1.tags) + self.assertEqual("This is clip 1", event1.clip_name) + self.assertEqual("Track 1", event1.track_name) + self.assertEqual("Test Session", event1.session_name) + self.assertEqual( + dict( + A="A", + Color="Blue", + Ver="1.1", + Mode="2", + Comment="This is some text in the comments", + Part="1", + ), + event1.tags, + ) self.assertEqual(Fraction(3600, 1), event1.start) event2 = next(events) - self.assertEqual("This is the second clip ...and this is the last clip", event2.clip_name) - self.assertEqual('Track 1', event2.track_name) - self.assertEqual('Test Session', event2.session_name) - self.assertEqual(dict(R='Noise', A='A', B='B', - Color='Red', - Comment='This is some text in the comments', - N='1', Mode='2', - Ver='1.1', - M1='M1', - Part='2'), event2.tags) + self.assertEqual( + "This is the second clip ...and this is the last clip", event2.clip_name + ) + self.assertEqual("Track 1", event2.track_name) + self.assertEqual("Test Session", event2.session_name) + self.assertEqual( + dict( + R="Noise", + A="A", + B="B", + Color="Red", + Comment="This is some text in the comments", + N="1", + Mode="2", + Ver="1.1", + M1="M1", + Part="2", + ), + event2.tags, + ) - self.assertEqual(c.session.header.convert_timecode('01:00:01:10'), event2.start) - self.assertEqual(c.session.header.convert_timecode('01:00:03:00'), event2.finish) + self.assertEqual(c.session.header.convert_timecode("01:00:01:10"), event2.start) + self.assertEqual( + c.session.header.convert_timecode("01:00:03:00"), event2.finish + ) self.assertIsNone(next(events, None)) @@ -51,76 +67,94 @@ class TestTagCompiler(unittest.TestCase): all_tags = c.compile_tag_list() - self.assertTrue(all_tags['Mode'] == {'2', '1'}) + self.assertTrue(all_tags["Mode"] == {"2", "1"}) @staticmethod def make_test_session(): - test_header = doc_entity.HeaderDescriptor(session_name="Test Session $Ver=1.1", - sample_rate=48000, - timecode_format="24", - timecode_drop_frame=False, - bit_depth=24, - start_timecode='00:59:00:00', - count_audio_tracks=1, - count_clips=3, - count_files=0 - ) + test_header = doc_entity.HeaderDescriptor( + session_name="Test Session $Ver=1.1", + sample_rate=48000, + timecode_format="24", + timecode_drop_frame=False, + bit_depth=24, + start_timecode="00:59:00:00", + count_audio_tracks=1, + count_clips=3, + count_files=0, + ) test_clips = [ - doc_entity.TrackClipDescriptor(channel=1, event=1, - clip_name='This is clip 1 {Color=Blue} $Mode=2', - start_time='01:00:00:00', - finish_time='01:00:01:03', - duration='00:00:01:03', - state='Unmuted', - timestamp=None), - doc_entity.TrackClipDescriptor(channel=1, event=2, - clip_name='This is the second clip {R=Noise} [B] $Mode=2', - start_time='01:00:01:10', - finish_time='01:00:02:00', - duration='00:00:00:14', - state='Unmuted', - timestamp=None), - doc_entity.TrackClipDescriptor(channel=1, event=3, - clip_name='& ...and this is the last clip $N=1 $Mode=2', - start_time='01:00:02:00', - finish_time='01:00:03:00', - duration='00:00:01:00', - state='Unmuted', - timestamp=None), + doc_entity.TrackClipDescriptor( + channel=1, + event=1, + clip_name="This is clip 1 {Color=Blue} $Mode=2", + start_time="01:00:00:00", + finish_time="01:00:01:03", + duration="00:00:01:03", + state="Unmuted", + timestamp=None, + ), + doc_entity.TrackClipDescriptor( + channel=1, + event=2, + clip_name="This is the second clip {R=Noise} [B] $Mode=2", + start_time="01:00:01:10", + finish_time="01:00:02:00", + duration="00:00:00:14", + state="Unmuted", + timestamp=None, + ), + doc_entity.TrackClipDescriptor( + channel=1, + event=3, + clip_name="& ...and this is the last clip $N=1 $Mode=2", + start_time="01:00:02:00", + finish_time="01:00:03:00", + duration="00:00:01:00", + state="Unmuted", + timestamp=None, + ), ] test_track = doc_entity.TrackDescriptor( index=0, name="Track 1 [A] {Color=Red} $Mode=1", - comments="{Comment=This is some text in the comments}", - user_delay_samples=0, - plugins=[], - state=[], - clips=test_clips) + comments="{Comment=This is some text in the comments}", + user_delay_samples=0, + plugins=[], + state=[], + clips=test_clips, + ) - markers = [doc_entity.MarkerDescriptor(number=1, - location="01:00:00:00", - time_reference=48000 * 3600, - units="Samples", - name="Marker 1 {Part=1}", - comments="", track_marker=False, - ), - doc_entity.MarkerDescriptor(number=2, - location="01:00:01:00", - time_reference=48000 * 3601, - units="Samples", - name="Marker 2 {Part=2}", - comments="[M1]", track_marker=False, - ), - ] + markers = [ + doc_entity.MarkerDescriptor( + number=1, + location="01:00:00:00", + time_reference=48000 * 3600, + units="Samples", + name="Marker 1 {Part=1}", + comments="", + track_marker=False, + ), + doc_entity.MarkerDescriptor( + number=2, + location="01:00:01:00", + time_reference=48000 * 3601, + units="Samples", + name="Marker 2 {Part=2}", + comments="[M1]", + track_marker=False, + ), + ] - test_session = doc_entity.SessionDescriptor(header=test_header, - tracks=[test_track], - clips=[], - files=[], - markers=markers, - plugins=[]) + test_session = doc_entity.SessionDescriptor( + header=test_header, + tracks=[test_track], + clips=[], + files=[], + markers=markers, + plugins=[], + ) return test_session -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unittests/test_tag_interpreter.py b/tests/unittests/test_tag_interpreter.py index 59cb33e..d6d1f6f 100644 --- a/tests/unittests/test_tag_interpreter.py +++ b/tests/unittests/test_tag_interpreter.py @@ -1,6 +1,6 @@ import unittest -from ptulsconv.docparser.tagged_string_parser_visitor import parse_tags, TagPreModes +from ptulsconv.docparser.tagged_string_parser_visitor import TagPreModes, parse_tags class TestTagInterpreter(unittest.TestCase): @@ -17,12 +17,12 @@ class TestTagInterpreter(unittest.TestCase): def test_tags(self): s1 = parse_tags("{a=100}") - self.assertEqual(s1.tag_dict['a'], "100") + self.assertEqual(s1.tag_dict["a"], "100") s2 = parse_tags("{b=This is a test} [option] $X=9") - self.assertEqual(s2.tag_dict['b'], 'This is a test') - self.assertEqual(s2.tag_dict['option'], 'option') - self.assertEqual(s2.tag_dict['X'], "9") + self.assertEqual(s2.tag_dict["b"], "This is a test") + self.assertEqual(s2.tag_dict["option"], "option") + self.assertEqual(s2.tag_dict["X"], "9") def test_modes(self): s1 = parse_tags("@ Monday Tuesday {a=1}") @@ -35,5 +35,5 @@ class TestTagInterpreter(unittest.TestCase): self.assertEqual(s3.mode, TagPreModes.APPEND) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unittests/test_tagging.py b/tests/unittests/test_tagging.py index fd9a516..0f611dd 100644 --- a/tests/unittests/test_tagging.py +++ b/tests/unittests/test_tagging.py @@ -1,15 +1,18 @@ -import unittest -from ptulsconv.docparser import doc_entity, pt_doc_parser, tag_compiler import os.path +import unittest + +from ptulsconv.docparser import doc_entity, pt_doc_parser, tag_compiler class TaggingIntegratedTests(unittest.TestCase): - path = os.path.dirname(__file__) + '/../export_cases/Tag Tests/Tag Tests.txt' + path = os.path.dirname(__file__) + "/../export_cases/Tag Tests/Tag Tests.txt" def test_event_list(self): - with open(self.path, 'r') as f: + with open(self.path, "r") as f: document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read()) - document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast) + document: doc_entity.SessionDescriptor = ( + pt_doc_parser.DocParserVisitor().visit(document_ast) + ) compiler = tag_compiler.TagCompiler() compiler.session = document @@ -27,9 +30,11 @@ class TaggingIntegratedTests(unittest.TestCase): self.assertEqual("Region 04", events[8].clip_name) def test_append(self): - with open(self.path, 'r') as f: + with open(self.path, "r") as f: document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read()) - document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast) + document: doc_entity.SessionDescriptor = ( + pt_doc_parser.DocParserVisitor().visit(document_ast) + ) compiler = tag_compiler.TagCompiler() compiler.session = document @@ -39,20 +44,26 @@ class TaggingIntegratedTests(unittest.TestCase): self.assertEqual("Dolor sic amet the rain in spain", events[2].clip_name) - self.assertEqual(document.header.convert_timecode("01:00:10:00"), events[2].start) - self.assertEqual(document.header.convert_timecode("01:00:25:00"), events[2].finish) + self.assertEqual( + document.header.convert_timecode("01:00:10:00"), events[2].start + ) + self.assertEqual( + document.header.convert_timecode("01:00:25:00"), events[2].finish + ) - self.assertIn('X', events[2].tags.keys()) - self.assertIn('ABC', events[2].tags.keys()) - self.assertIn('A', events[2].tags.keys()) - self.assertEqual('302', events[2].tags['X']) - self.assertEqual('ABC', events[2].tags['ABC']) - self.assertEqual('1', events[2].tags['A']) + self.assertIn("X", events[2].tags.keys()) + self.assertIn("ABC", events[2].tags.keys()) + self.assertIn("A", events[2].tags.keys()) + self.assertEqual("302", events[2].tags["X"]) + self.assertEqual("ABC", events[2].tags["ABC"]) + self.assertEqual("1", events[2].tags["A"]) def test_successive_appends(self): - with open(self.path, 'r') as f: + with open(self.path, "r") as f: document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read()) - document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast) + document: doc_entity.SessionDescriptor = ( + pt_doc_parser.DocParserVisitor().visit(document_ast) + ) compiler = tag_compiler.TagCompiler() compiler.session = document @@ -62,9 +73,13 @@ class TaggingIntegratedTests(unittest.TestCase): self.assertEqual("A B C", events[3].clip_name) - self.assertEqual(document.header.convert_timecode("01:00:15:00"), events[3].start) - self.assertEqual(document.header.convert_timecode("01:00:45:00"), events[3].finish) + self.assertEqual( + document.header.convert_timecode("01:00:15:00"), events[3].start + ) + self.assertEqual( + document.header.convert_timecode("01:00:45:00"), events[3].finish + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/unittests/test_utils.py b/tests/unittests/test_utils.py index 78e59ca..c245d69 100644 --- a/tests/unittests/test_utils.py +++ b/tests/unittests/test_utils.py @@ -8,13 +8,13 @@ class MyTestCase(unittest.TestCase): v = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] expected = [1, 2, 7, 5, 6, 15, 9, 10] - should = (lambda x, y: y % 4 == 0) - do_combine = (lambda x, y: x + y) + should = lambda x, y: y % 4 == 0 + do_combine = lambda x, y: x + y r = apply_appends(iter(v), should, do_combine) r1 = list(r) self.assertEqual(r1, expected) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()