Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
shell: bash -l {0}
run: |
micromamba run -n oceanarray python -m pip install --upgrade pip
micromamba run -n oceanarray python -m pip install -e . --no-deps --force-reinstall
micromamba run -n oceanarray python -m pip install -e ".[dev]" --force-reinstall

- name: Build documentation
shell: bash -l {0}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
shell: bash -l {0}
run: |
micromamba run -n oceanarray python -m pip install --upgrade pip
micromamba run -n oceanarray python -m pip install -e . --no-deps --force-reinstall
micromamba run -n oceanarray python -m pip install -e ".[dev]" --force-reinstall

- name: Build documentation
shell: bash -l {0}
Expand Down
9 changes: 6 additions & 3 deletions docs/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ dependencies:
- python=3.11
- pip
- pandoc
- pip:
# dev extra = runtime (incl. seasenselib) + test + docs + pdf + ruff
- -e .[dev]
# The editable install (`-e .[dev]`, dev extra = runtime incl. seasenselib +
# test + docs + pdf + ruff) is done in the workflow's "Install package
# (editable)" step, not here: setup-micromamba runs an environment-file pip
# section from the file's own directory (docs/), where `.` has no
# pyproject.toml. The run step executes from the repo root, so the path
# resolves correctly and cannot drift with the action's working directory.
22 changes: 20 additions & 2 deletions oceanarray/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,9 @@ def cmd_report(args: argparse.Namespace) -> int:
_html_dir = paths.resolve_report_dir(
args.mooring, getattr(args, "outdir", None), report_dir, proc_root
)
pdf_path = _html_dir / f"{args.mooring}_report.pdf"
pdf_path = paths.resolve_pdf_path(
args.mooring, getattr(args, "pdf_dir", None), _html_dir
)
print(f"PDF: {pdf_path} (combined from the HTML pages above)")
return 0

Expand Down Expand Up @@ -484,8 +486,14 @@ def cmd_report(args: argparse.Namespace) -> int:
html_dir = paths.resolve_report_dir(
args.mooring, getattr(args, "outdir", None), report_dir, proc_root
)
# --pdf-dir redirects the PDF to a shared directory; when unset it stays
# beside the HTML. Resolved identically to the dry-run preview above.
pdf_path = paths.resolve_pdf_path(
args.mooring, getattr(args, "pdf_dir", None), html_dir
)
pdf_path.parent.mkdir(parents=True, exist_ok=True)
try:
pdf_path = combine_mooring_pdf(html_dir, args.mooring)
pdf_path = combine_mooring_pdf(html_dir, args.mooring, output_path=pdf_path)
_status("file", str(pdf_path))
except (ImportError, FileNotFoundError) as exc:
# An explicit --pdf request that cannot be honoured is a failure.
Expand Down Expand Up @@ -1216,6 +1224,16 @@ def build_parser() -> argparse.ArgumentParser:
"({mooring}_report.pdf). Requires the 'pdf' extra: pip install "
"oceanarray[pdf]. Implied by --all.",
)
p_report.add_argument(
"--pdf-dir",
dest="pdf_dir",
default=None,
metavar="DIR",
help="Write the combined PDF to DIR/{mooring}_report.pdf instead of beside "
"the HTML pages, so every mooring's PDF collects in one shareable directory "
"(created if needed). Only affects PDF placement; still needs --pdf or --all "
"to build the PDF at all.",
)
p_report.add_argument(
"--array",
action="store_true",
Expand Down
42 changes: 42 additions & 0 deletions oceanarray/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,48 @@ def resolve_report_dir(
return mooring_proc_dir(proc_root, mooring) / "report"


def resolve_pdf_path(
mooring: str,
pdf_dir: Optional[_PathLike],
report_html_dir: _PathLike,
) -> Path:
"""Return the path a mooring's combined report PDF is written to.

Single source of truth for PDF output-path resolution, mirrored by both
``cmd_report``'s dry-run preview and its real PDF branch so the two never
drift. When *pdf_dir* is set, every mooring's PDF collects in that one
shareable directory as ``pdf_dir/<mooring>_report.pdf``; when it is ``None``
the PDF stays beside the HTML pages at
``report_html_dir/<mooring>_report.pdf``.

This function is pure: it does not create any directory (so the dry-run
preview stays side-effect-free). The caller creates the parent before
writing.

Parameters
----------
mooring : str
Mooring name, used as the filename stem.
pdf_dir : str or Path, optional
Central directory collecting every mooring's PDF (``--pdf-dir``); when
set it takes precedence over the beside-the-HTML default.
report_html_dir : str or Path
The mooring's resolved HTML report directory (from
:func:`resolve_report_dir`); the default PDF location when *pdf_dir* is
unset.

Returns
-------
Path
The resolved ``<mooring>_report.pdf`` path.

"""
filename = f"{mooring}_report.pdf"
if pdf_dir:
return Path(pdf_dir) / filename
return Path(report_html_dir) / filename


def raw_mooring_dir(raw_root: _PathLike, mooring: str) -> Path:
"""Return the mooring-level raw-data directory.

Expand Down
13 changes: 12 additions & 1 deletion oceanarray/reports/_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,20 @@
(e.g. NetCDF global attributes) fills the space under its heading and
continues overleaf, instead of jumping whole to the next page and orphaning
the heading with a gap above it. Individual rows stay intact and the header
row repeats on each page. */
row repeats on each page.

``table-layout: fixed`` (with an explicit width, required for it to engage) is
a large PERFORMANCE fix, not cosmetics: with the default ``auto`` layout
WeasyPrint scans every cell to compute intrinsic column widths, and a single
huge cell — e.g. the ~7.7 KB one-line seasenselib ``raw-opaque`` provenance
JSON in an ADCP global-attributes table — makes that O(content) pass
pathological (one page went from 0.3 s to 47 s). Fixed layout skips it:
columns size from the first row, long values wrap. Overall report render
dropped ~50 s → ~6 s. Print-only, so on-screen tables are unaffected. */
table {
break-inside: auto;
table-layout: fixed;
width: 100%;
}
thead {
display: table-header-group;
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ def test_build_parser_version_action_present():
"pdf",
False,
),
(
["report", "mymoor", "--proc-dir", "/tmp/p", "--pdf-dir", "/tmp/pdfs"],
"pdf_dir",
"/tmp/pdfs",
),
(
["report", "mymoor", "--proc-dir", "/tmp/p"],
"pdf_dir",
None,
),
],
)
def test_build_parser_parses_basic_args(args, attr, expected):
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/test_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
mooring_proc_dir,
raw_mooring_dir,
require_current_layout,
resolve_pdf_path,
resolve_report_dir,
safe_serial,
stage_output_name,
Expand All @@ -40,6 +41,28 @@ def test_default_is_proc_mooring_report(self) -> None:
assert resolve_report_dir("M1", None, None, "/proc") == Path("/proc/M1/report")


class TestResolvePdfPath:
"""PDF output-path resolution: pdf_dir/<mooring>_report.pdf > beside the HTML."""

def test_default_is_beside_html(self) -> None:
"""With no pdf_dir, the PDF sits beside the HTML pages."""
assert resolve_pdf_path("M1", None, "/proc/M1/report") == Path(
"/proc/M1/report/M1_report.pdf"
)

def test_pdf_dir_wins(self) -> None:
"""A pdf_dir collects every mooring's PDF under one directory."""
assert resolve_pdf_path("M1", "/shared", "/proc/M1/report") == Path(
"/shared/M1_report.pdf"
)

def test_pure_no_directory_created(self, tmp_path) -> None:
"""The resolver is side-effect-free: it never creates pdf_dir."""
target = tmp_path / "does_not_exist"
resolve_pdf_path("M1", target, tmp_path)
assert not target.exists()


@pytest.mark.parametrize(
("raw", "expected"),
[
Expand Down
Loading