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