Bug ruff 2026 (#19)

* Updated Ruff to v0.16 and fixed for default checks 

* Nudge version and removed license classifier

---------

Co-authored-by: Jamie Hardt <jamie@squad51.us>
This commit is contained in:
Jamie Hardt
2026-09-19 16:35:56 -07:00
committed by GitHub
parent 2fc8a653cd
commit e0e3024280
39 changed files with 2031 additions and 1582 deletions
+1
View File
@@ -109,3 +109,4 @@ lcov.info
.vim
.vscode
uv.lock
+1
View File
@@ -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
+14 -17
View File
@@ -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 -------------------------------------------------
+10 -4
View File
@@ -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",
+47 -23
View File
@@ -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)
# RPR_ShowConsoleMsg(output_path)
+65 -70
View File
@@ -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__":
+45 -28
View File
@@ -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}"
+116 -96
View File
@@ -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())
+1 -4
View File
@@ -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
+74 -71
View File
@@ -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
+84 -78
View File
@@ -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"]
+84 -63
View File
@@ -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
+98 -74
View File
@@ -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
+41 -34
View File
@@ -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
@@ -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]
+6 -4
View File
@@ -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
+171 -122
View File
@@ -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 "<GRect x=%f y=%f width=%f height=%f>" % \
(self.x, self.y, self.width, self.height)
return f"<GRect x={self.x} y={self.y} width={self.width} height={self.height}>"
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)
+34 -29
View File
@@ -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 = "<em>%s</em><br />%s" % (tc_format.seconds_to_smpte(scene[2]),
tc_format.seconds_to_smpte(scene[3]))
tc_data = f"<em>{tc_format.seconds_to_smpte(scene[2])}</em><br />{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))
+282 -220
View File
@@ -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)
+1
View File
@@ -1,5 +1,6 @@
# TODO: Complete Recordist Log
def output_report(records):
# order by start
+102 -73
View File
@@ -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 += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "EFF")
bg_color = "red"
tag_field += (
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>EFF</font> "
)
elif line.tv:
bg_color = 'blue'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "TV")
bg_color = "blue"
tag_field += (
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>TV</font> "
)
elif line.adlib:
bg_color = 'purple'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "ADLIB")
bg_color = "purple"
tag_field += (
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>ADLIB</font> "
)
elif line.optional:
bg_color = 'green'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font>" \
% (bg_color, fg_color, "OPTIONAL")
bg_color = "green"
tag_field += f"font backColor={bg_color} textColor={fg_color} fontSize=11>OPTIONAL</font>"
entries.append(tag_field)
return "<br />".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<br /><font fontSize=7>%s</font>" \
% (line.cue_number, line.character_name)
cue_number_field = (
f"{line.cue_number}<br /><font fontSize=7>{line.character_name}</font>"
)
time_data = time_format(line.time_budget_mins)
if line.priority is not None:
time_data = time_data + "<br />" + "P: " + line.priority
time_data = time_data + "<br />" + "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("<u>" + this_scene + "</u>", scene_style),
line_table]))
story.append(
KeepTogether(
[
Spacer(1.0, 0.25 * inch),
Paragraph("<u>" + this_scene + "</u>", 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) + "<br />" + \
tc_format.seconds_to_smpte(line.finish)
tc_data = (
tc_format.seconds_to_smpte(line.start)
+ "<br />"
+ 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 + "<br/>" + " ".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:
+182 -130
View File
@@ -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,
)
+48 -44
View File
@@ -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),
]
)
)
+19 -19
View File
@@ -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")
+32 -29
View File
@@ -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)
+128 -97
View File
@@ -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,
)
+58 -35
View File
@@ -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()
+20 -16
View File
@@ -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()
+18 -12
View File
@@ -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()
+12 -9
View File
@@ -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")
+11 -14
View File
@@ -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()
+24 -21
View File
@@ -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()
+10 -4
View File
@@ -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()
+14 -12
View File
@@ -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()
+2 -2
View File
@@ -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)
+116 -82
View File
@@ -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()
+6 -6
View File
@@ -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()
+35 -20
View File
@@ -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()
+3 -3
View File
@@ -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()