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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,7 @@ data/moor/raw/msm76_2018/sbe56/DSE18_SBE05606397_2018_08_27.cnv
data/moor/raw/msm76_2018/sbe56/DSE18_SBE05606401_2018_08_27.cnv
data/moor/raw/msm76_2018/sbe56/DSE18_SBE05606402_2018_08_27.cnv
data/moor/raw/msm76_2018/sbe56/DSE18_SBE05606409_2018_08_27.cnv

# Generated report output under test fixtures (HTML + multi-MB PDFs) — regenerated
# by `oceanarray report ... --pdf`; never commit into history.
tests/fixtures/proc/*/report/
17 changes: 13 additions & 4 deletions docs/source/cli_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,9 @@ description of what each report page contains.
oceanarray report MOORING [--raw-dir DIR] [--proc-dir DIR]
[-o DIR] [--report-dir DIR]
[--instruments] [--stack] [--grid] [--all]
[--serial SN ...] [--array] [--cruise-table]
[--sig-level SIG ...] [-n] [--force]
[--skip-existing]
[--pdf] [--serial SN ...] [--array]
[--cruise-table] [--sig-level SIG ...]
[-n] [--force] [--skip-existing]

**Flags**

Expand Down Expand Up @@ -273,7 +273,16 @@ description of what each report page contains.
* - ``--all`` / ``-A``
- flag
- off
- Generate all report pages. Equivalent to ``--stack --grid --instruments``.
- Generate all report pages and the combined PDF. Equivalent to
``--stack --grid --instruments --pdf``.
* - ``--pdf``
- flag
- off
- Combine the generated HTML report pages into a single A4 PDF
(``{mooring}_report.pdf``), in reading order
summary → instruments → stack → grid. Requires the optional ``pdf``
extra (``pip install oceanarray[pdf]``, which installs WeasyPrint).
Implied by ``--all``.
* - ``--serial SN``
- string (repeat)
- (all)
Expand Down
3 changes: 2 additions & 1 deletion docs/source/project_structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ oceanarray/
│ │ ├── _array.py # Array-level multi-mooring summary report
│ │ ├── _plots.py # Tier 3: report-level figure wrappers (base64 PNGs)
│ │ ├── _html_helpers.py # HTML/QC constants, base64 helpers, NC metadata readers
│ │ └── _recovery_table.py # Per-mooring cruise-report recovery table
│ │ ├── _recovery_table.py # Per-mooring cruise-report recovery table
│ │ └── _pdf.py # combine_mooring_pdf: HTML pages → single A4 PDF (WeasyPrint)
│ │
│ ├── tools/ # [core] Shared I/O infrastructure
│ │ ├── readers.py # NetCDF and legacy-format instrument readers
Expand Down
28 changes: 28 additions & 0 deletions docs/source/reports.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,34 @@ Generated when ``--grid`` is passed. Requires ``{mooring}_grid.nc`` (run

----

PDF output
----------

The HTML reports can be combined into a single A4 PDF for printing or
archiving. The HTML pages remain the single source of truth — the PDF is a
post-processing step (via `WeasyPrint <https://weasyprint.org/>`_) that applies
a print stylesheet (A4 page size, page numbers, page-break avoidance, hidden
navigation buttons) without altering report generation.

PDF output requires the optional ``pdf`` extra::

pip install oceanarray[pdf]

Build the PDF alongside the HTML pages with ``--pdf`` (or ``--all``, which
implies it)::

# Combine whatever HTML pages exist into MOORING_report.pdf
oceanarray report MOORING --raw-dir $RAW --proc-dir $PROC --pdf

# Generate every page and the combined PDF in one go
oceanarray report MOORING --raw-dir $RAW --proc-dir $PROC --all

Pages are concatenated in reading order — summary → per-instrument → stack →
grid — and only pages that exist on disk are included. The result is written
to ``{mooring}_report.pdf`` in the same directory as the HTML pages.

----

Report colour conventions
--------------------------

Expand Down
42 changes: 41 additions & 1 deletion oceanarray/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,23 @@ def cmd_report(args: argparse.Namespace) -> int:
print(
f" Instrument {instr_type:12s} s/n {serial:8s} {p.name} ({'exists' if p.exists() else 'new'})"
)
if getattr(args, "pdf", False) or all_reports:
_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"
print(f"PDF: {pdf_path} (combined from the HTML pages above)")
return 0

if getattr(args, "array", False):
from .report._array import generate_array_report

if getattr(args, "pdf", False) or getattr(args, "all_reports", False):
_status(
"error",
"--pdf is not supported in --array mode; the array index is "
"HTML-only. Run 'report MOORING --pdf' per mooring instead.",
)
# Resolve the YAML path: try as-given first, then relative to proc_dir.
_yaml_path = Path(args.mooring)
if not _yaml_path.exists() and not _yaml_path.is_absolute():
Expand Down Expand Up @@ -388,6 +400,25 @@ def cmd_report(args: argparse.Namespace) -> int:
out_path=out_dir / f"{args.mooring}_recovery_table.html",
force=args.force,
)
if getattr(args, "pdf", False) or all_reports:
from .report import combine_mooring_pdf

# Combine reads the same directory generate() wrote to; both resolve it
# through paths.resolve_report_dir so they can never drift.
html_dir = paths.resolve_report_dir(
args.mooring, getattr(args, "outdir", None), report_dir, proc_root
)
try:
pdf_path = combine_mooring_pdf(html_dir, args.mooring)
_status("file", str(pdf_path))
except (ImportError, FileNotFoundError) as exc:
# An explicit --pdf request that cannot be honoured is a failure.
# When the PDF was only implied by --all, treat it as best-effort:
# warn but keep the (successful) HTML result and do not fail, so
# `report --all` still works on machines without the pdf extra.
_status("error", str(exc))
if getattr(args, "pdf", False):
return 1
return 0 if result else 1
finally:
if _sigma_restore is not None:
Expand Down Expand Up @@ -1215,7 +1246,16 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
default=False,
dest="all_reports",
help="Generate all report pages: equivalent to --stack --grid --instruments",
help="Generate all report pages: equivalent to --stack --grid --instruments "
"--pdf (also builds the combined PDF)",
)
p_report.add_argument(
"--pdf",
action="store_true",
default=False,
help="Combine the generated HTML report pages into a single A4 PDF "
"({mooring}_report.pdf). Requires the 'pdf' extra: pip install "
"oceanarray[pdf]. Implied by --all.",
)
p_report.add_argument(
"--array",
Expand Down
40 changes: 39 additions & 1 deletion oceanarray/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import re
import sys
from pathlib import Path
from typing import Any, Union
from typing import Any, Optional, Union

_PathLike = Union[str, Path]

Expand Down Expand Up @@ -39,6 +39,44 @@ def mooring_proc_dir(proc_root: _PathLike, mooring: str) -> Path:
return Path(proc_root) / mooring


def resolve_report_dir(
mooring: str,
outdir: Optional[_PathLike],
report_dir: Optional[_PathLike],
proc_root: _PathLike,
) -> Path:
"""Return the directory a mooring's HTML report pages are written to.

Single source of truth for report output-dir resolution, mirrored by both
:meth:`oceanarray.report.MooringReport.generate` and the PDF combiner so the
two never drift. Priority: explicit *outdir* wins; otherwise a central
*report_dir* nests each mooring under ``report_dir/<mooring>``; otherwise the
default ``proc_root/<mooring>/report``.

Parameters
----------
mooring : str
Mooring name.
outdir : str or Path, optional
Explicit output directory (``--output-dir``); takes precedence when set.
report_dir : str or Path, optional
Central report root (``--report-dir``); each mooring nests below it.
proc_root : str or Path
Cruise-level processed-data root, used for the default location.

Returns
-------
Path
The resolved report directory.

"""
if outdir:
return Path(outdir)
if report_dir:
return Path(report_dir) / mooring
return mooring_proc_dir(proc_root, mooring) / "report"


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

Expand Down
3 changes: 2 additions & 1 deletion oceanarray/report/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Mooring report package — public API."""

from ._mooring import MooringReport
from ._pdf import combine_mooring_pdf

__all__ = ["MooringReport"]
__all__ = ["MooringReport", "combine_mooring_pdf"]
11 changes: 4 additions & 7 deletions oceanarray/report/_mooring.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@
<td>{% if loop.index0 == 0 %}{{ instr.instr_type }}{% endif %}</td>
<td>{% if loop.index0 == 0 %}<code>{{ instr.serial }}</code>{% endif %}</td>
<td>{{ sensor.sensor_type | title }}</td>
<td style="font-size:0.8rem">{{ sensor.sensor_model }}</td>
<td>{{ sensor.sensor_model }}</td>
<td><code>{{ sensor.sensor_serial }}</code></td>
<td>{{ sensor.cal_date }}</td>
<td>
Expand Down Expand Up @@ -1067,12 +1067,9 @@ def generate(
print(f"ERROR: Processing directory not found: {proc_dir}")
return None

if outdir:
out_dir = Path(outdir)
elif self._report_dir is not None:
out_dir = self._report_dir / mooring_name
else:
out_dir = proc_dir / "report"
out_dir = paths.resolve_report_dir(
mooring_name, outdir, self._report_dir, self._proc_dir
)
out_dir.mkdir(parents=True, exist_ok=True)
output_path = out_dir / f"{mooring_name}_report.html"
yaml_path = proc_dir / f"{mooring_name}.mooring.yaml"
Expand Down
Loading
Loading