bridge.cli SourceΒΆ

  1__all__ = [
  2    "generate_paperlike_crf_pdf",
  3    "generate_paperlike_crf_word",
  4]
  5
  6
  7# -- IMPORTS --
  8
  9# -- Standard libraries --
 10import sys
 11from datetime import datetime
 12from pathlib import Path
 13
 14# -- 3rd party libraries --
 15import click
 16import pandas as pd
 17
 18# -- Internal libraries --
 19import bridge.generate_pdf.paper_crf as paper_crf
 20import bridge.generate_pdf.paper_word as paper_word
 21
 22from bridge.arc.arc_api import ArcApiClientError
 23from bridge.utils.logger import setup_logger
 24
 25
 26logger = setup_logger(__name__)
 27
 28
 29@click.command
 30@click.option(
 31    "--data-dictionary-csv",
 32    required=True,
 33    help="Path (absolute or relative) to the data dictionary CSV",
 34)
 35@click.option(
 36    "--arc-version",
 37    default="1.2.2",
 38    required=False,
 39    help="Optional ARC version if not using custom paperlike details and supplemental phrases, defaults to the latest (currently `1.2.2`)",
 40)
 41@click.option(
 42    "--redcap-db-name",
 43    default="Generic",
 44    required=False,
 45    help="Optional REDCap project DB name, defaults to `Generic`",
 46)
 47@click.option(
 48    "--language",
 49    default="English",
 50    required=False,
 51    help="Optional PDF language, defaults to English",
 52)
 53@click.option(
 54    "--paperlike-details-csv",
 55    required=False,
 56    help="Optional path (absolute or relative) to a custom paperlike form details CSV",
 57)
 58@click.option(
 59    "--supplemental-phrases-csv",
 60    required=False,
 61    help="Optional path (absolute or relative) to a custom supplemental phrases CSV",
 62)
 63@click.option(
 64    "--output-path",
 65    required=False,
 66    help="Optional path to write the PDF file, defaults to ./output/CRF-<redcap_db_name>-<language>-<YYYYMMDD timestamp>.pdf",
 67)
 68def generate_paperlike_crf_pdf(
 69    data_dictionary_csv: str,
 70    arc_version: str | None = "1.2.2",
 71    redcap_db_name: str | None = "Generic",
 72    language: str | None = "English",
 73    paperlike_details_csv: str | None = None,
 74    supplemental_phrases_csv: str | None = None,
 75    output_path: str | None = None,
 76) -> bytes:
 77    """:py:class:`bytes` : Returns a PDF of the paperlike CRF.
 78
 79    Parameters
 80    ----------
 81    data_dictionary_csv : str
 82        The local path to the data dictionary CSV file.
 83
 84    arc_version : str, default="1.2.2"
 85            Optional ARC version string, defaults to the current latest version
 86            ``"1.2.2"`` (as of 15.05.2026).
 87
 88    redcap_db_name : str, default=""
 89            Optional REDCap database name, defaults to ``""``.
 90
 91    language : str, default="English"
 92            Optional PDF language setting, defaults to ``"English"``.
 93
 94    paperlike_details_csv : str, default=None
 95        Optional paperlike form details CSV, defaults to ``None``.
 96
 97    supplemental_phrases_csv : str, default=None
 98        Optional supplemental phrases CSV, defaults to ``None``.
 99
100    output_path : str, default=None
101            Optional output path string, defaults to ``None``. If ``None`` then
102            output file is created in a subfolder named ``output`` created in
103            the working directory.
104
105    Returns
106    -------
107    bytes
108            The CRF PDF object as bytes.
109    """
110    # Load the data dictionary
111    data_dictionary = pd.read_csv(Path(data_dictionary_csv).resolve())
112
113    logger.info(
114        f"Data dictionary {data_dictionary_csv} loaded with {len(data_dictionary)} rows."
115    )
116
117    # Main conditional logic to support ARC vs non-ARC loading of paperlike
118    # form details and supplemental phrases.
119    if not (paperlike_details_csv and supplemental_phrases_csv):
120        # Call the Bridge function to get the CRF PDF with ARC-based logic, as
121        # at least one of the user-defined paperlike form details and
122        # supplemental phrases CSVs must be null at this point. The function
123        # defines a default ARC version of ``"1.2.2"`` so the ARC version will
124        # never be null here.
125        try:
126            pdf = paper_crf.generate_paperlike_pdf(
127                data_dictionary=data_dictionary,
128                version=arc_version,
129                db_name=redcap_db_name,
130                language=language,
131            )
132        except ArcApiClientError as e:
133            logger.error(e)
134            sys.exit(1)
135    else:
136        # Load the user-defined paperlike details and supplmental phrases CSVs
137        paperlike_details = pd.read_csv(paperlike_details_csv)
138        supplemental_phrases = pd.read_csv(supplemental_phrases_csv)
139        # Call the Bridge function to get the CRF PDF with non-ARC logic
140        pdf = paper_crf.generate_paperlike_pdf(
141            data_dictionary=data_dictionary,
142            db_name=redcap_db_name,
143            language=language,
144            paperlike_details=paperlike_details,
145            supplemental_phrases=supplemental_phrases,
146        )
147
148    logger.info(f"Paperlike CRF PDF (size {sys.getsizeof(pdf)} bytes) generated.")
149
150    timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
151
152    # Create the output folder if it doesn't exist, and form the output file
153    # path.
154    if not output_path:
155        if not Path("output").exists():
156            Path("output").mkdir()
157        output_path = (
158            Path("output").joinpath(
159                f"CRF-{redcap_db_name}-{arc_version}-{language}-{timestamp}.pdf"
160            )
161            if not (paperlike_details_csv and supplemental_phrases_csv)
162            else Path("output").joinpath(
163                f"CRF-{redcap_db_name}-{language}-{timestamp}.pdf"
164            )
165        )
166    else:
167        output_path = Path(output_path).resolve()
168
169    # Write the PDF document to the output file, before returning it.
170    output_path.write_bytes(pdf)
171
172    logger.info(f"Paperlike CRF PDF written to file {output_path}.")
173
174    return pdf
175
176
177@click.command
178@click.option(
179    "--data-dictionary-csv",
180    required=True,
181    help="Path (absolute or relative) to the data dictionary CSV",
182)
183@click.option(
184    "--include-descriptive-rows",
185    required=False,
186    is_flag=True,
187    default=False,
188    help="Include source rows with descriptive field type, defaults to `False`",
189)
190@click.option(
191    "--output-path",
192    required=False,
193    help="Optional path to write the Word file, defaults to ./output/CRF-<YYYYMMDD timestamp>.docx",
194)
195def generate_paperlike_crf_word(
196    data_dictionary_csv: str,
197    include_descriptive_rows: bool = False,
198    output_path: str | Path | None = None,
199) -> bytes:
200    """:py:class:`bytes` : Returns a Word document (``.docx``) of the paperlike CRF.
201
202    Parameters
203    ----------
204    data_dictionary_csv : str
205            The local path to the data dictionary CSV file.
206
207    include_descriptive_rows : bool, default=False
208            Include source rows with descriptive field type.
209
210    output_path : str, default=None
211            Optional output path string, defaults to ``None``. If ``None`` then
212            output file is created in a subfolder named ``output`` created in
213            the working directory.
214
215    Returns
216    -------
217    bytes
218            The CRF Word document object as bytes.
219    """
220    # Load the data dictionary
221    data_dictionary = pd.read_csv(Path(data_dictionary_csv).resolve())
222
223    logger.info(
224        f"Data dictionary {data_dictionary_csv} loaded with {len(data_dictionary)} rows."
225    )
226
227    # Call the Bridge function to get the CRF Word document.
228    word = paper_word.df_to_word(
229        data_dictionary, include_descriptive_rows=include_descriptive_rows
230    )
231
232    logger.info(
233        f"Paperlike CRF Word document (size {sys.getsizeof(word)} bytes) generated, with option to include descriptive rows set to {include_descriptive_rows}."
234    )
235
236    timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
237
238    # Create the output folder if it doesn't exist, and form the output file
239    # path.
240    if not output_path:
241        if not Path("output").exists():
242            Path("output").mkdir()
243        output_path = Path("output").joinpath(f"CRF-{timestamp}.docx")
244    else:
245        output_path = Path(output_path).resolve()
246
247    # Write the Word document to the output file, before returning it.
248    output_path.write_bytes(word)
249
250    logger.info(f"Paperlike CRF Word document written to file {output_path}.")
251
252    return word