csrlite 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,461 @@
1
+ # pyre-strict
2
+ """
3
+ Protocol Deviation (PD) Listing Functions
4
+
5
+ This module provides functions for generating detailed PD listings showing individual
6
+ protocol deviation records with key details like category, term, and coded term.
7
+
8
+ The two-step pipeline:
9
+ - pd_listing_ard: Filter, select, sort, and rename columns (returns display-ready data)
10
+ - pd_listing_rtf: Generate formatted RTF output
11
+ - pd_listing: Complete pipeline wrapper
12
+ - study_plan_to_pd_listing: Batch generation from StudyPlan
13
+
14
+ Uses Polars native SQL capabilities for data manipulation and parse.py utilities
15
+ for StudyPlan parsing.
16
+ """
17
+
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional, Tuple
20
+
21
+ import polars as pl
22
+ from rtflite import RTFBody, RTFColumnHeader, RTFDocument, RTFFootnote, RTFPage, RTFSource, RTFTitle
23
+
24
+ from ..common.parse import StudyPlanParser
25
+ from ..common.plan import StudyPlan
26
+ from ..common.utils import apply_common_filters
27
+
28
+
29
+ def pd_listing_ard(
30
+ population: pl.DataFrame,
31
+ observation: pl.DataFrame,
32
+ population_filter: Optional[str],
33
+ observation_filter: Optional[str],
34
+ id: Tuple[str, str],
35
+ population_columns: Optional[List[Tuple[str, str]]] = None,
36
+ observation_columns: Optional[List[Tuple[str, str]]] = None,
37
+ sort_columns: Optional[List[str]] = None,
38
+ page_by: Optional[List[str]] = None,
39
+ ) -> pl.DataFrame:
40
+ """
41
+ Generate Analysis Results Data (ARD) for PD listing.
42
+
43
+ Filters and joins population and observation data, then selects relevant columns.
44
+
45
+ Args:
46
+ population: Population DataFrame (subject-level data, e.g., ADSL)
47
+ observation: Observation DataFrame (event data, e.g., ADPD)
48
+ population_filter: SQL WHERE clause for population (can be None)
49
+ observation_filter: SQL WHERE clause for observation (can be None)
50
+ id: Tuple (variable_name, label) for ID column
51
+ population_columns: List of tuples (variable_name, label) from population
52
+ (e.g., [("SEX", "Sex"), ("RACE", "Race")])
53
+ observation_columns: List of tuples (variable_name, label) from observation
54
+ (e.g., [("DVCAT", "Category")])
55
+ sort_columns: List of column names to sort by. If None, sorts by id column.
56
+ page_by: List of column names to page by.
57
+
58
+ Returns:
59
+ pl.DataFrame: Filtered and joined records with selected columns
60
+ """
61
+ id_var_name, id_var_label = id
62
+
63
+ # Apply common filters
64
+ # Note: parameter_filter is None for PD as it usually doesn't have a PARAM column like AE
65
+ population_filtered, observation_to_filter = apply_common_filters(
66
+ population=population,
67
+ observation=observation,
68
+ population_filter=population_filter,
69
+ observation_filter=observation_filter,
70
+ parameter_filter=None,
71
+ )
72
+
73
+ assert observation_to_filter is not None
74
+
75
+ # Filter observation to include only subjects in filtered population
76
+ observation_filtered = observation_to_filter.filter(
77
+ pl.col(id_var_name).is_in(population_filtered[id_var_name].to_list())
78
+ )
79
+
80
+ # Determine which observation columns to select
81
+ if observation_columns is None:
82
+ # Default: select id column only
83
+ obs_cols = [id_var_name]
84
+ else:
85
+ # Extract variable names from tuples
86
+ obs_col_names = [var_name for var_name, _ in observation_columns]
87
+ # Ensure id is included
88
+ obs_cols = [id_var_name] + [col for col in obs_col_names if col != id_var_name]
89
+
90
+ # Select available observation columns
91
+ obs_cols_available = [col for col in obs_cols if col in observation_filtered.columns]
92
+ result = observation_filtered.select(obs_cols_available)
93
+
94
+ # Join with population to add population columns
95
+ if population_columns is not None:
96
+ # Extract variable names from tuples
97
+ pop_col_names = [var_name for var_name, _ in population_columns]
98
+ # Select id + requested population columns
99
+ pop_cols = [id_var_name] + [col for col in pop_col_names if col != id_var_name]
100
+ pop_cols_available = [col for col in pop_cols if col in population_filtered.columns]
101
+ population_subset = population_filtered.select(pop_cols_available)
102
+
103
+ # Left join to preserve all observation records
104
+ result = result.join(population_subset, on=id_var_name, how="left")
105
+
106
+ # Create __index__ column for pagination
107
+ # Default to using the id column as the index
108
+ if id_var_name in result.columns:
109
+ result = result.with_columns(
110
+ (pl.lit(f"{id_var_label} = ") + pl.col(id_var_name).cast(pl.Utf8)).alias("__index__")
111
+ )
112
+
113
+ # Use page_by columns if provided and they exist
114
+ existing_page_by_cols = [col for col in page_by if col in result.columns] if page_by else []
115
+
116
+ if existing_page_by_cols:
117
+ # Create a mapping from column name to label
118
+ column_labels = {id_var_name: id_var_label}
119
+ if population_columns:
120
+ for var_name, var_label in population_columns:
121
+ column_labels[var_name] = var_label
122
+
123
+ # Ensure the order of labels matches the order of columns in page_by
124
+ index_expressions = []
125
+ for col_name in existing_page_by_cols:
126
+ label = column_labels.get(col_name, col_name)
127
+ index_expressions.append(pl.lit(f"{label} = ") + pl.col(col_name).cast(pl.Utf8))
128
+
129
+ result = result.with_columns(
130
+ pl.concat_str(index_expressions, separator=", ").alias("__index__")
131
+ )
132
+
133
+ page_by_remove = [col for col in (page_by or []) if col != id_var_name]
134
+ result = result.drop(page_by_remove)
135
+
136
+ if "__index__" in result.columns:
137
+ # Get all columns except __index__
138
+ other_columns = [col for col in result.columns if col != "__index__"]
139
+ # Reorder to have __index__ first
140
+ result = result.select(["__index__"] + other_columns)
141
+
142
+ # Sort by specified columns or default to id column
143
+ if sort_columns is None:
144
+ # Default: sort by id column if it exists in result
145
+ if id_var_name in result.columns:
146
+ result = result.sort(id_var_name)
147
+ else:
148
+ # Sort by specified columns that exist in result
149
+ cols_to_sort = [col for col in sort_columns if col in result.columns]
150
+ if cols_to_sort:
151
+ result = result.sort(cols_to_sort)
152
+
153
+ return result
154
+
155
+
156
+ def pd_listing_rtf(
157
+ df: pl.DataFrame,
158
+ column_labels: Dict[str, str],
159
+ title: List[str],
160
+ footnote: Optional[List[str]],
161
+ source: Optional[List[str]],
162
+ col_rel_width: Optional[List[float]] = None,
163
+ group_by: Optional[List[str]] = None,
164
+ page_by: Optional[List[str]] = None,
165
+ orientation: str = "landscape",
166
+ ) -> RTFDocument:
167
+ """
168
+ Generate RTF table from PD listing display DataFrame.
169
+
170
+ Creates a formatted RTF table with column headers and optional section grouping/pagination.
171
+
172
+ Args:
173
+ df: Display DataFrame from pd_listing_ard
174
+ column_labels: Dictionary mapping column names to display labels
175
+ title: Title(s) for the table as list of strings
176
+ footnote: Optional footnote(s) as list of strings
177
+ source: Optional source note(s) as list of strings
178
+ col_rel_width: Optional list of relative column widths. If None, auto-calculated
179
+ as equal widths for all columns
180
+ group_by: Optional list of column names to group by for section headers within pages.
181
+ Should only contain population columns (e.g., ["TRT01A", "USUBJID"])
182
+ page_by: Optional list of column names to trigger new pages when values change.
183
+ Should only contain population columns (e.g., ["TRT01A"])
184
+ orientation: Page orientation ("portrait" or "landscape"), default is "landscape"
185
+
186
+ Returns:
187
+ RTFDocument: RTF document object that can be written to file
188
+ """
189
+ # Calculate number of columns
190
+ n_cols = len(df.columns)
191
+
192
+ # Build column headers using labels
193
+ col_header = [column_labels.get(col, col) for col in df.columns]
194
+
195
+ # Calculate column widths
196
+ if col_rel_width is None:
197
+ col_widths = [1.0] * n_cols
198
+ else:
199
+ col_widths = col_rel_width
200
+
201
+ # Normalize title, footnote, source to lists
202
+ title_list = title
203
+ footnote_list: List[str] = footnote or []
204
+ source_list: List[str] = source or []
205
+
206
+ # Build RTF document
207
+ rtf_components: Dict[str, Any] = {
208
+ "df": df,
209
+ "rtf_page": RTFPage(orientation=orientation),
210
+ "rtf_title": RTFTitle(text=title_list),
211
+ "rtf_column_header": [
212
+ RTFColumnHeader(
213
+ text=col_header[1:],
214
+ col_rel_width=col_widths[1:],
215
+ text_justification=["l"] + ["l"] * (n_cols - 1), # Default left align for PD
216
+ ),
217
+ ],
218
+ "rtf_body": RTFBody(
219
+ col_rel_width=col_widths,
220
+ text_justification=["l"] * n_cols,
221
+ border_left=["single"],
222
+ border_top=["single"] + [""] * (n_cols - 1),
223
+ border_bottom=["single"] + [""] * (n_cols - 1),
224
+ group_by=group_by,
225
+ page_by=page_by,
226
+ ),
227
+ }
228
+
229
+ # Add optional footnote
230
+ if footnote_list:
231
+ rtf_components["rtf_footnote"] = RTFFootnote(text=footnote_list)
232
+
233
+ # Add optional source
234
+ if source_list:
235
+ rtf_components["rtf_source"] = RTFSource(text=source_list)
236
+
237
+ # Create RTF document
238
+ doc = RTFDocument(**rtf_components)
239
+
240
+ return doc
241
+
242
+
243
+ def pd_listing(
244
+ population: pl.DataFrame,
245
+ observation: pl.DataFrame,
246
+ population_filter: Optional[str],
247
+ observation_filter: Optional[str],
248
+ id: Tuple[str, str],
249
+ title: List[str],
250
+ footnote: Optional[List[str]],
251
+ source: Optional[List[str]],
252
+ output_file: str,
253
+ population_columns: Optional[List[Tuple[str, str]]] = None,
254
+ observation_columns: Optional[List[Tuple[str, str]]] = None,
255
+ sort_columns: Optional[List[str]] = None,
256
+ group_by: Optional[List[str]] = None,
257
+ page_by: Optional[List[str]] = None,
258
+ col_rel_width: Optional[List[float]] = None,
259
+ orientation: str = "landscape",
260
+ ) -> str:
261
+ """
262
+ Complete PD listing pipeline wrapper.
263
+
264
+ This function orchestrates the two-step pipeline:
265
+ 1. pd_listing_ard: Filter, join, select, and sort columns
266
+ 2. pd_listing_rtf: Generate RTF output with optional grouping/pagination
267
+
268
+ Args:
269
+ population: Population DataFrame (subject-level data, e.g., ADSL)
270
+ observation: Observation DataFrame (event data, e.g., ADPD)
271
+ population_filter: SQL WHERE clause for population (can be None)
272
+ observation_filter: SQL WHERE clause for observation (can be None)
273
+ id: Tuple (variable_name, label) for ID column
274
+ title: Title for RTF output as list of strings
275
+ footnote: Optional footnote for RTF output as list of strings
276
+ source: Optional source for RTF output as list of strings
277
+ output_file: File path to write RTF output
278
+ population_columns: Optional list of tuples (variable_name, label) from population
279
+ observation_columns: Optional list of tuples (variable_name, label) from observation
280
+ sort_columns: Optional list of column names to sort by. If None, sorts by id column.
281
+ group_by: Optional list of column names to group by for section headers
282
+ (population columns only)
283
+ page_by: Optional list of column names to trigger new pages (population columns only)
284
+ col_rel_width: Optional column widths for RTF output
285
+ orientation: Page orientation ("portrait" or "landscape"), default is "landscape"
286
+ """
287
+ # Step 1: Generate ARD (includes filtering, joining, and selecting)
288
+ df = pd_listing_ard(
289
+ population=population,
290
+ observation=observation,
291
+ population_filter=population_filter,
292
+ observation_filter=observation_filter,
293
+ id=id,
294
+ population_columns=population_columns,
295
+ observation_columns=observation_columns,
296
+ sort_columns=sort_columns,
297
+ page_by=page_by,
298
+ )
299
+
300
+ # Build column labels from tuples
301
+ id_var_name, id_var_label = id
302
+ column_labels = {id_var_name: id_var_label}
303
+
304
+ # Add observation column labels
305
+ if observation_columns is not None:
306
+ for var_name, var_label in observation_columns:
307
+ column_labels[var_name] = var_label
308
+
309
+ # Add population column labels
310
+ if population_columns is not None:
311
+ for var_name, var_label in population_columns:
312
+ column_labels[var_name] = var_label
313
+
314
+ # Set __index__ header to empty string
315
+ column_labels["__index__"] = ""
316
+
317
+ # Step 2: Generate RTF and write to file
318
+ rtf_doc = pd_listing_rtf(
319
+ df=df,
320
+ column_labels=column_labels,
321
+ title=title,
322
+ footnote=footnote,
323
+ source=source,
324
+ col_rel_width=col_rel_width,
325
+ group_by=group_by,
326
+ page_by=["__index__"],
327
+ orientation=orientation,
328
+ )
329
+ rtf_doc.write_rtf(output_file)
330
+
331
+ return output_file
332
+
333
+
334
+ def study_plan_to_pd_listing(
335
+ study_plan: StudyPlan,
336
+ ) -> List[str]:
337
+ """
338
+ Generate PD listing RTF outputs for all analyses defined in StudyPlan.
339
+
340
+ This function reads the expanded plan from StudyPlan and generates
341
+ an RTF listing for each pd_listing analysis specification automatically.
342
+
343
+ Args:
344
+ study_plan: StudyPlan object with loaded datasets and analysis specifications
345
+
346
+ Returns:
347
+ List[str]: List of paths to generated RTF files
348
+ """
349
+
350
+ # Meta data
351
+ analysis = "pd_listing"
352
+ output_dir = study_plan.output_dir
353
+ # Adjusted column widths for PD
354
+ col_rel_width = [1.0, 1.0, 1.0, 3.0, 2.0]
355
+ footnote = None
356
+ source = None
357
+
358
+ population_df_name = "adsl"
359
+ observation_df_name = "adpd"
360
+
361
+ id = ("USUBJID", "Subject ID")
362
+ # Column configuration with labels - easy to customize
363
+ # Population columns (demographics) - group variable will be added dynamically
364
+ population_columns_base: List[
365
+ Tuple[str, str]
366
+ ] = [] # Often empty for PD listing, maybe just ID and treatment
367
+
368
+ # Observation columns (event details)
369
+ # Using the columns identified from adpd.parquet
370
+ observation_columns_base = [
371
+ ("DVCAT", "Category"),
372
+ ("DVTERM", "Term"),
373
+ ("DVDECOD", "Coded Term"),
374
+ ]
375
+
376
+ # Sorting configuration
377
+ sort_columns = ["TRT01A", "USUBJID", "DVCAT", "DVTERM"]
378
+ page_by = ["USUBJID", "TRT01A"]
379
+ group_by = ["USUBJID"]
380
+
381
+ # Create output directory if it doesn't exist
382
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
383
+
384
+ # Initialize parser
385
+ parser = StudyPlanParser(study_plan)
386
+
387
+ # Get expanded plan DataFrame
388
+ plan_df = study_plan.get_plan_df()
389
+
390
+ # Filter for PD listing analyses
391
+ pd_plans = plan_df.filter(pl.col("analysis") == analysis)
392
+
393
+ rtf_files = []
394
+
395
+ # Generate RTF for each analysis
396
+ for row in pd_plans.iter_rows(named=True):
397
+ population = row["population"]
398
+ observation = row.get("observation")
399
+ group = row.get("group")
400
+
401
+ # Validate group is specified
402
+ if group is None:
403
+ raise ValueError(
404
+ f"Group not specified in YAML for analysis: population={population}, "
405
+ f"observation={observation}. "
406
+ "Please add group to your YAML plan."
407
+ )
408
+
409
+ # Get datasets using parser
410
+ population_df, observation_df = parser.get_datasets(population_df_name, observation_df_name)
411
+
412
+ # Get filters using parser
413
+ population_filter = parser.get_population_filter(population)
414
+ obs_filter = parser.get_observation_filter(observation)
415
+
416
+ # Get group variable name from YAML
417
+ group_var_name, group_labels = parser.get_group_info(group)
418
+
419
+ # Determine group variable label
420
+ group_var_label = group_labels[0] if group_labels else "Treatment"
421
+
422
+ # Build columns dynamically from base configuration with labels
423
+ population_columns = population_columns_base + [(group_var_name, group_var_label)]
424
+ observation_columns = observation_columns_base
425
+
426
+ # Build title with population and observation context
427
+ title_parts = ["Listing of Protocol Deviations"]
428
+
429
+ pop_kw = study_plan.keywords.populations.get(population)
430
+ if pop_kw and pop_kw.label:
431
+ title_parts.append(pop_kw.label)
432
+
433
+ # Build output filename
434
+ filename = f"{analysis}_{population}"
435
+ if observation:
436
+ filename += f"_{observation}"
437
+ filename += ".rtf"
438
+ output_file = str(Path(output_dir) / filename)
439
+
440
+ # Generate RTF
441
+ rtf_path = pd_listing(
442
+ population=population_df,
443
+ observation=observation_df,
444
+ population_filter=population_filter,
445
+ observation_filter=obs_filter,
446
+ id=id,
447
+ title=title_parts,
448
+ footnote=footnote,
449
+ source=source,
450
+ output_file=output_file,
451
+ population_columns=population_columns,
452
+ observation_columns=observation_columns,
453
+ sort_columns=sort_columns,
454
+ col_rel_width=col_rel_width,
455
+ group_by=group_by,
456
+ page_by=page_by,
457
+ )
458
+
459
+ rtf_files.append(rtf_path)
460
+
461
+ return rtf_files
@@ -1,68 +1,68 @@
1
- Metadata-Version: 2.4
2
- Name: csrlite
3
- Version: 0.2.1
4
- Summary: A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials
5
- Author-email: Clinical Biostatistics Team <biostat@example.com>, Ming-Chun Chen <hellomingchun@gmail.com>
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/elong0527/csrlite
8
- Project-URL: Documentation, https://elong0527.github.io/csrlite
9
- Project-URL: Repository, https://github.com/elong0527/csrlite.git
10
- Project-URL: Bug Tracker, https://github.com/elong0527/csrlite/issues
11
- Keywords: clinical-trials,biostatistics,yaml,tlf,tables,listings,figures
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Intended Audience :: Science/Research
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
19
- Requires-Python: >=3.10
20
- Description-Content-Type: text/markdown
21
- Requires-Dist: pydantic>=2.0.0
22
- Requires-Dist: pyyaml>=6.0
23
- Requires-Dist: polars>=0.20.0
24
- Requires-Dist: rtflite>=2.1.1
25
- Provides-Extra: rtf
26
- Requires-Dist: rtflite; extra == "rtf"
27
- Provides-Extra: plotting
28
- Requires-Dist: matplotlib>=3.5.0; extra == "plotting"
29
- Requires-Dist: plotly>=5.0.0; extra == "plotting"
30
- Provides-Extra: dev
31
- Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
32
- Requires-Dist: pytest>=9.0.1; extra == "dev"
33
- Requires-Dist: black>=22.0.0; extra == "dev"
34
- Requires-Dist: isort>=7.0.0; extra == "dev"
35
- Requires-Dist: ruff>=0.14.8; extra == "dev"
36
- Requires-Dist: mypy>=1.19.0; extra == "dev"
37
- Requires-Dist: quarto>=0.1.0; extra == "dev"
38
- Requires-Dist: pyre-check>=0.9.18; extra == "dev"
39
- Requires-Dist: jupyter>=1.1.1; extra == "dev"
40
- Requires-Dist: jupyter-cache>=1.0.1; extra == "dev"
41
- Requires-Dist: nbformat>=5.10.4; extra == "dev"
42
- Provides-Extra: all
43
- Requires-Dist: rtflite; extra == "all"
44
- Requires-Dist: matplotlib>=3.5.0; extra == "all"
45
- Requires-Dist: plotly>=5.0.0; extra == "all"
46
-
47
- # csrlite
48
-
49
- [![CI](https://github.com/elong0527/csrlite/actions/workflows/ci.yml/badge.svg)](https://github.com/elong0527/csrlite/actions/workflows/ci.yml)
50
- [![codecov](https://codecov.io/gh/elong0527/csrlite/branch/main/graph/badge.svg)](https://codecov.io/gh/elong0527/csrlite)
51
- [![PyPI version](https://badge.fury.io/py/csrlite.svg)](https://badge.fury.io/py/csrlite)
52
- [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
53
-
54
- A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials.
55
-
56
- ## Installation
57
-
58
- ```bash
59
- pip install csrlite
60
- ```
61
-
62
- ## Documentation
63
-
64
- Visit [https://elong0527.github.io/csrlite](https://elong0527.github.io/csrlite) for full documentation.
65
-
66
- ## License
67
-
68
- MIT License - see [LICENSE](LICENSE) file for details.
1
+ Metadata-Version: 2.4
2
+ Name: csrlite
3
+ Version: 0.3.0
4
+ Summary: A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials
5
+ Author-email: Yilong Zhang <elong0527@gmail.com>, Ming-Chun Chen <hellomingchun@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/elong0527/csrlite
8
+ Project-URL: Documentation, https://elong0527.github.io/csrlite
9
+ Project-URL: Repository, https://github.com/elong0527/csrlite.git
10
+ Project-URL: Bug Tracker, https://github.com/elong0527/csrlite/issues
11
+ Keywords: clinical-trials,biostatistics,yaml,tlf,tables,listings,figures
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: pydantic>=2.0.0
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: polars>=0.20.0
24
+ Requires-Dist: rtflite>=2.1.1
25
+ Provides-Extra: rtf
26
+ Requires-Dist: rtflite; extra == "rtf"
27
+ Provides-Extra: plotting
28
+ Requires-Dist: matplotlib>=3.5.0; extra == "plotting"
29
+ Requires-Dist: plotly>=5.0.0; extra == "plotting"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
32
+ Requires-Dist: pytest>=9.0.1; extra == "dev"
33
+ Requires-Dist: black>=22.0.0; extra == "dev"
34
+ Requires-Dist: isort>=7.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.14.8; extra == "dev"
36
+ Requires-Dist: mypy>=1.19.0; extra == "dev"
37
+ Requires-Dist: quarto>=0.1.0; extra == "dev"
38
+ Requires-Dist: pyre-check>=0.9.18; extra == "dev"
39
+ Requires-Dist: jupyter>=1.1.1; extra == "dev"
40
+ Requires-Dist: jupyter-cache>=1.0.1; extra == "dev"
41
+ Requires-Dist: nbformat>=5.10.4; extra == "dev"
42
+ Provides-Extra: all
43
+ Requires-Dist: rtflite; extra == "all"
44
+ Requires-Dist: matplotlib>=3.5.0; extra == "all"
45
+ Requires-Dist: plotly>=5.0.0; extra == "all"
46
+
47
+ # csrlite
48
+
49
+ [![CI](https://github.com/elong0527/csrlite/actions/workflows/ci.yml/badge.svg)](https://github.com/elong0527/csrlite/actions/workflows/ci.yml)
50
+ [![codecov](https://codecov.io/gh/elong0527/csrlite/branch/main/graph/badge.svg)](https://codecov.io/gh/elong0527/csrlite)
51
+ [![PyPI version](https://badge.fury.io/py/csrlite.svg)](https://badge.fury.io/py/csrlite)
52
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
53
+
54
+ A hierarchical YAML-based framework for generating Tables, Listings, and Figures in clinical trials.
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install csrlite
60
+ ```
61
+
62
+ ## Documentation
63
+
64
+ Visit [https://elong0527.github.io/csrlite](https://elong0527.github.io/csrlite) for full documentation.
65
+
66
+ ## License
67
+
68
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -0,0 +1,26 @@
1
+ csrlite/__init__.py,sha256=ykct5bSThhEniaNwvSdxfr0UgFynWOtGPRmKlARR82Y,2492
2
+ csrlite/ae/__init__.py,sha256=gZHPLATRF9f8QBwwQtEjQRtXMsqOJsUK2sbUMLjiE5U,14
3
+ csrlite/ae/ae_listing.py,sha256=R4g8JnJRCx4u60xRC2IEu8EPzEIgxj8VgDhBaaQ5eZE,18389
4
+ csrlite/ae/ae_specific.py,sha256=_CDAgF4vMmjpqFTppL2LgmExVsEhA57z4jYj4Y1zLfY,17022
5
+ csrlite/ae/ae_summary.py,sha256=46IyuqHGdn0dLOrz7XffKNcNjscA0Y8OZFiZ6akisB4,13692
6
+ csrlite/ae/ae_utils.py,sha256=ew5Mm_zNdflc_MRYvYSChXhRhGQ1oZcz7H_TZPVvFBk,2011
7
+ csrlite/cm/cm_listing.py,sha256=X8lzzjNOnp2qRbBVmtyqhcKH03-zJ8hhPFzeDUsO59w,18407
8
+ csrlite/cm/cm_summary.py,sha256=DQz77ophWX2mX7cILFx3_e8eP9vwcRaHc5VxmwMiECc,10152
9
+ csrlite/common/config.py,sha256=FUnUL1BtQO52U0ag1U_d2K3UP5L_vA_KifonANHLv_c,878
10
+ csrlite/common/count.py,sha256=k1W-LdQv63s-B-Oeq2SvYsXctrT1YMVWs93CtIaGpVw,8785
11
+ csrlite/common/parse.py,sha256=Vz9C7ljkDygT2qkP6TlY3T3p71D6BD5GtIwRKv6p8ps,9319
12
+ csrlite/common/plan.py,sha256=XXUGpzNxC6oS66c7NYnDPmE0CwXMhIQzlJCga1nDktw,12928
13
+ csrlite/common/rtf.py,sha256=R_tSebzyiHHYG_M7i8jD6FOjd1sPp37sZy9yZJ9Dp4g,4303
14
+ csrlite/common/utils.py,sha256=It0aHqPfXDmCte2uVAO2Lkb3U_jDLrjNihAL8gziTQk,1110
15
+ csrlite/common/yaml_loader.py,sha256=_v9pkbAUVshTqVoMLqMiEn17awL2K0kFR4pdDArMSOM,3071
16
+ csrlite/disposition/__init__.py,sha256=KMtGoBjN4aKNYvXHmZ0GX-f4RnmQ3coYbUrkFeU8Es0,85
17
+ csrlite/disposition/disposition.py,sha256=HW4B78LzrQ4i7O9Dtqhk7Kbh9EjusqGTtLPR6xTFDvI,10347
18
+ csrlite/ie/ie_listing.py,sha256=7USDfEigey8RCyn9GZaKWOK1rn2NuE6ijJPZltmLKZM,3050
19
+ csrlite/ie/ie_summary.py,sha256=lPXkt8HMO_l-5KVtMbSPRx4qmuMxXoGflKSATJ_Wlz4,9392
20
+ csrlite/mh/mh_listing.py,sha256=7Nplm9Xca70rhLLNTmfwAGrPw4Vb1FFIRDNjAylciDs,6084
21
+ csrlite/mh/mh_summary.py,sha256=l5l3ApSrxnZeBiJYwvuT2pqbJFKb2y_4-kcP-XPBeWo,9932
22
+ csrlite/pd/pd_listing.py,sha256=omYo5zE7hg6yvqhC1JxwBPZ3dNHeA4vF2UPDFj-mRQ4,17171
23
+ csrlite-0.3.0.dist-info/METADATA,sha256=SvJkOn0RYM9c2fT-ck8JVeRO2joR0-pOJPEXN_imcDs,2828
24
+ csrlite-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ csrlite-0.3.0.dist-info/top_level.txt,sha256=59zJTvGH5zx2FY4vCl4kgnH8awT0cZrg21Mace7IFlU,8
26
+ csrlite-0.3.0.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- csrlite/__init__.py,sha256=zKceiP6Z47_o4GUjVLhIqPO6iMefyj6BGKffVHLUTjs,1729
2
- csrlite/ae/__init__.py,sha256=Pp661SGbsoYoXLmd24dUQB_lx1jYKWEsCEPjgVg5UUU,15
3
- csrlite/ae/ae_listing.py,sha256=fZWhYmi_Vzp974z-3o7EQ9joizEjF0mio2eB5_I8ghU,18883
4
- csrlite/ae/ae_specific.py,sha256=P1glCd5K_ur_KexxbVaC79BmowgQSMI9K8xdzL6OmIM,17505
5
- csrlite/ae/ae_summary.py,sha256=Vk4bUonnjAji2zeJkiPHW7nwgpwUG1UfbuzH4M0w1MM,14093
6
- csrlite/ae/ae_utils.py,sha256=tOVOdsj-TRTw1w5T4JqFTYUwTfOppZWGlRHi6nkNWRA,2073
7
- csrlite/common/config.py,sha256=qWFTNSaeWnqAagXM6FwACdQafXLG_nFWxFHEk5dAo-I,912
8
- csrlite/common/count.py,sha256=T9nB9hYdc2jIIuV9HISK2ZS0wXNV_FUBDCKW_VVM1fQ,9078
9
- csrlite/common/parse.py,sha256=J4UkmNlG10mrJ-O68-po1LlfLchK_kSvfdU1GOaAYgQ,9627
10
- csrlite/common/plan.py,sha256=tYnK-5TVCoGWobxkqmasYYYkc9DJlHmOiTIcrP3MFRo,13293
11
- csrlite/common/rtf.py,sha256=g1n765i2HPpwkh6aQOzlw4UmCC21-JMZEezFtUaO6U8,4440
12
- csrlite/common/utils.py,sha256=XLUQqYtEm4SLNImvNDxAuj9OOu6Eogm3_uIp0vyfpfQ,1143
13
- csrlite/common/yaml_loader.py,sha256=ZPgXw5X9eitMHCPEJBHzeYtcjesEQ8sP0hOFSYnzrgk,3142
14
- csrlite/disposition/__init__.py,sha256=AiLW7Os7SBFrvvkaUMrxYEntK15_XXT0HYpZQxBx7ro,87
15
- csrlite/disposition/disposition.py,sha256=W1Oik5hqDfalQ69NXullH7XYWP3oRw2x2nFuFWlE3Qo,10679
16
- csrlite/ie/ie.py,sha256=9kEAe4aY3xfHe7O-o2ZD3LNfL5eTha2ZPkAflopjPYU,13544
17
- csrlite-0.2.1.dist-info/METADATA,sha256=zstZU0eiNMFz4ICMeOf9VqCvUGywxMkXTg0hCqGe9HY,2911
18
- csrlite-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- csrlite-0.2.1.dist-info/top_level.txt,sha256=59zJTvGH5zx2FY4vCl4kgnH8awT0cZrg21Mace7IFlU,8
20
- csrlite-0.2.1.dist-info/RECORD,,