csrlite 0.1.0__py3-none-any.whl → 0.2.1__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.
- csrlite/__init__.py +71 -50
- csrlite/ae/__init__.py +1 -1
- csrlite/ae/ae_listing.py +494 -492
- csrlite/ae/ae_specific.py +483 -478
- csrlite/ae/ae_summary.py +401 -399
- csrlite/ae/ae_utils.py +62 -132
- csrlite/common/config.py +34 -0
- csrlite/common/count.py +293 -199
- csrlite/common/parse.py +308 -308
- csrlite/common/plan.py +365 -353
- csrlite/common/rtf.py +137 -0
- csrlite/common/utils.py +33 -33
- csrlite/common/yaml_loader.py +71 -71
- csrlite/disposition/__init__.py +2 -2
- csrlite/disposition/disposition.py +332 -301
- csrlite/ie/ie.py +405 -0
- {csrlite-0.1.0.dist-info → csrlite-0.2.1.dist-info}/METADATA +68 -68
- csrlite-0.2.1.dist-info/RECORD +20 -0
- csrlite-0.1.0.dist-info/RECORD +0 -17
- {csrlite-0.1.0.dist-info → csrlite-0.2.1.dist-info}/WHEEL +0 -0
- {csrlite-0.1.0.dist-info → csrlite-0.2.1.dist-info}/top_level.txt +0 -0
csrlite/common/rtf.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# pyre-strict
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
from rtflite import RTFBody, RTFColumnHeader, RTFDocument, RTFFootnote, RTFPage, RTFSource, RTFTitle
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_rtf_table_n_pct(
|
|
9
|
+
df: pl.DataFrame,
|
|
10
|
+
col_header_1: list[str],
|
|
11
|
+
col_header_2: list[str] | None,
|
|
12
|
+
col_widths: list[float] | None,
|
|
13
|
+
title: list[str] | str,
|
|
14
|
+
footnote: list[str] | str | None,
|
|
15
|
+
source: list[str] | str | None,
|
|
16
|
+
borders_2: bool = True,
|
|
17
|
+
orientation: str = "landscape",
|
|
18
|
+
) -> RTFDocument:
|
|
19
|
+
"""
|
|
20
|
+
Create a standardized RTF table document with 1 or 2 header rows.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
df: Polars DataFrame containing the table data.
|
|
24
|
+
col_header_1: List of strings for the first header row.
|
|
25
|
+
col_header_2: Optional list of strings for the second header row.
|
|
26
|
+
col_widths: Optional list of relative column widths. Defaults to equal widths.
|
|
27
|
+
title: Title string or list of title strings.
|
|
28
|
+
footnote: Footnote string or list of footnote strings.
|
|
29
|
+
source: Source string or list of source strings.
|
|
30
|
+
borders_2: Whether to show borders for the second header row. Defaults to True.
|
|
31
|
+
orientation: Page orientation, "landscape" or "portrait". Defaults to "landscape".
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
RTFDocument object.
|
|
35
|
+
"""
|
|
36
|
+
n_cols = len(df.columns)
|
|
37
|
+
|
|
38
|
+
# Calculate column widths if None - simple default
|
|
39
|
+
if col_widths is None:
|
|
40
|
+
col_widths = [1.0] * n_cols
|
|
41
|
+
|
|
42
|
+
# Normalize metadata
|
|
43
|
+
title_list = [title] if isinstance(title, str) else title
|
|
44
|
+
footnote_list = [footnote] if isinstance(footnote, str) else (footnote or [])
|
|
45
|
+
source_list = [source] if isinstance(source, str) else (source or [])
|
|
46
|
+
|
|
47
|
+
headers = [
|
|
48
|
+
RTFColumnHeader(
|
|
49
|
+
text=col_header_1,
|
|
50
|
+
col_rel_width=col_widths,
|
|
51
|
+
text_justification=["l"] + ["c"] * (n_cols - 1),
|
|
52
|
+
)
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
if col_header_2:
|
|
56
|
+
h2_kwargs = {
|
|
57
|
+
"text": col_header_2,
|
|
58
|
+
"col_rel_width": col_widths,
|
|
59
|
+
"text_justification": ["l"] + ["c"] * (n_cols - 1),
|
|
60
|
+
}
|
|
61
|
+
if borders_2:
|
|
62
|
+
h2_kwargs["border_left"] = ["single"]
|
|
63
|
+
h2_kwargs["border_top"] = [""]
|
|
64
|
+
|
|
65
|
+
headers.append(RTFColumnHeader(**h2_kwargs))
|
|
66
|
+
|
|
67
|
+
rtf_components: dict[str, Any] = {
|
|
68
|
+
"df": df,
|
|
69
|
+
"rtf_page": RTFPage(orientation=orientation),
|
|
70
|
+
"rtf_title": RTFTitle(text=title_list),
|
|
71
|
+
"rtf_column_header": headers,
|
|
72
|
+
"rtf_body": RTFBody(
|
|
73
|
+
col_rel_width=col_widths,
|
|
74
|
+
text_justification=["l"] + ["c"] * (n_cols - 1),
|
|
75
|
+
border_left=["single"] * n_cols,
|
|
76
|
+
),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if footnote_list:
|
|
80
|
+
rtf_components["rtf_footnote"] = RTFFootnote(text=footnote_list)
|
|
81
|
+
|
|
82
|
+
if source_list:
|
|
83
|
+
rtf_components["rtf_source"] = RTFSource(text=source_list)
|
|
84
|
+
|
|
85
|
+
return RTFDocument(**rtf_components)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_rtf_listing(
|
|
89
|
+
df: pl.DataFrame,
|
|
90
|
+
col_header: list[str],
|
|
91
|
+
col_widths: list[float] | None,
|
|
92
|
+
title: list[str] | str,
|
|
93
|
+
footnote: list[str] | str | None,
|
|
94
|
+
source: list[str] | str | None,
|
|
95
|
+
orientation: str = "landscape",
|
|
96
|
+
) -> RTFDocument:
|
|
97
|
+
"""
|
|
98
|
+
Create a standardized RTF listing document.
|
|
99
|
+
"""
|
|
100
|
+
n_cols = len(df.columns)
|
|
101
|
+
|
|
102
|
+
# Calculate column widths if None
|
|
103
|
+
if col_widths is None:
|
|
104
|
+
col_widths = [1.0] * n_cols
|
|
105
|
+
|
|
106
|
+
# Normalize metadata
|
|
107
|
+
title_list = [title] if isinstance(title, str) else title
|
|
108
|
+
footnote_list = [footnote] if isinstance(footnote, str) else (footnote or [])
|
|
109
|
+
source_list = [source] if isinstance(source, str) else (source or [])
|
|
110
|
+
|
|
111
|
+
headers = [
|
|
112
|
+
RTFColumnHeader(
|
|
113
|
+
text=col_header,
|
|
114
|
+
col_rel_width=col_widths,
|
|
115
|
+
text_justification=["l"] * n_cols,
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
rtf_components: dict[str, Any] = {
|
|
120
|
+
"df": df,
|
|
121
|
+
"rtf_page": RTFPage(orientation=orientation),
|
|
122
|
+
"rtf_title": RTFTitle(text=title_list),
|
|
123
|
+
"rtf_column_header": headers,
|
|
124
|
+
"rtf_body": RTFBody(
|
|
125
|
+
col_rel_width=col_widths,
|
|
126
|
+
text_justification=["l"] * n_cols,
|
|
127
|
+
border_left=["single"] * n_cols,
|
|
128
|
+
),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if footnote_list:
|
|
132
|
+
rtf_components["rtf_footnote"] = RTFFootnote(text=footnote_list)
|
|
133
|
+
|
|
134
|
+
if source_list:
|
|
135
|
+
rtf_components["rtf_source"] = RTFSource(text=source_list)
|
|
136
|
+
|
|
137
|
+
return RTFDocument(**rtf_components)
|
csrlite/common/utils.py
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
# pyre-strict
|
|
2
|
-
import polars as pl
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def apply_common_filters(
|
|
6
|
-
population: pl.DataFrame,
|
|
7
|
-
observation: pl.DataFrame,
|
|
8
|
-
population_filter: str | None,
|
|
9
|
-
observation_filter: str | None,
|
|
10
|
-
parameter_filter: str | None = None,
|
|
11
|
-
) -> tuple[pl.DataFrame, pl.DataFrame]:
|
|
12
|
-
"""
|
|
13
|
-
Apply standard population, observation, and parameter filters.
|
|
14
|
-
|
|
15
|
-
Returns:
|
|
16
|
-
Tuple of (filtered_population, filtered_observation_pre_id_match)
|
|
17
|
-
"""
|
|
18
|
-
# Apply population filter
|
|
19
|
-
if population_filter:
|
|
20
|
-
population_filtered = population.filter(pl.sql_expr(population_filter))
|
|
21
|
-
else:
|
|
22
|
-
population_filtered = population
|
|
23
|
-
|
|
24
|
-
# Apply observation filter
|
|
25
|
-
observation_filtered = observation
|
|
26
|
-
if observation_filter:
|
|
27
|
-
observation_filtered = observation_filtered.filter(pl.sql_expr(observation_filter))
|
|
28
|
-
|
|
29
|
-
# Apply parameter filter
|
|
30
|
-
if parameter_filter:
|
|
31
|
-
observation_filtered = observation_filtered.filter(pl.sql_expr(parameter_filter))
|
|
32
|
-
|
|
33
|
-
return population_filtered, observation_filtered
|
|
1
|
+
# pyre-strict
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def apply_common_filters(
|
|
6
|
+
population: pl.DataFrame,
|
|
7
|
+
observation: pl.DataFrame | None,
|
|
8
|
+
population_filter: str | None,
|
|
9
|
+
observation_filter: str | None,
|
|
10
|
+
parameter_filter: str | None = None,
|
|
11
|
+
) -> tuple[pl.DataFrame, pl.DataFrame | None]:
|
|
12
|
+
"""
|
|
13
|
+
Apply standard population, observation, and parameter filters.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Tuple of (filtered_population, filtered_observation_pre_id_match)
|
|
17
|
+
"""
|
|
18
|
+
# Apply population filter
|
|
19
|
+
if population_filter:
|
|
20
|
+
population_filtered = population.filter(pl.sql_expr(population_filter))
|
|
21
|
+
else:
|
|
22
|
+
population_filtered = population
|
|
23
|
+
|
|
24
|
+
# Apply observation filter
|
|
25
|
+
observation_filtered = observation
|
|
26
|
+
if observation_filter and observation_filtered is not None:
|
|
27
|
+
observation_filtered = observation_filtered.filter(pl.sql_expr(observation_filter))
|
|
28
|
+
|
|
29
|
+
# Apply parameter filter
|
|
30
|
+
if parameter_filter and observation_filtered is not None:
|
|
31
|
+
observation_filtered = observation_filtered.filter(pl.sql_expr(parameter_filter))
|
|
32
|
+
|
|
33
|
+
return population_filtered, observation_filtered
|
csrlite/common/yaml_loader.py
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
# pyre-strict
|
|
2
|
-
from copy import deepcopy
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Any, Dict, Optional
|
|
5
|
-
|
|
6
|
-
import yaml
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class YamlInheritanceLoader:
|
|
10
|
-
def __init__(self, base_path: Optional[Path] = None) -> None:
|
|
11
|
-
self.base_path: Path = base_path or Path(".")
|
|
12
|
-
|
|
13
|
-
def load(self, file_name: str) -> Dict[str, Any]:
|
|
14
|
-
"""
|
|
15
|
-
Load a YAML file by name relative to base_path and resolve inheritance.
|
|
16
|
-
"""
|
|
17
|
-
file_path = self.base_path / file_name
|
|
18
|
-
if not file_path.exists():
|
|
19
|
-
raise FileNotFoundError(f"YAML file not found: {file_path}")
|
|
20
|
-
|
|
21
|
-
with open(file_path, "r") as f:
|
|
22
|
-
data = yaml.safe_load(f) or {}
|
|
23
|
-
|
|
24
|
-
return self._resolve_inheritance(data)
|
|
25
|
-
|
|
26
|
-
def _resolve_inheritance(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
27
|
-
templates = data.get("study", {}).get("template", [])
|
|
28
|
-
if isinstance(templates, str):
|
|
29
|
-
templates = [templates]
|
|
30
|
-
|
|
31
|
-
if not templates:
|
|
32
|
-
return data
|
|
33
|
-
|
|
34
|
-
merged_template_data: Dict[str, Any] = {}
|
|
35
|
-
for template_file in templates:
|
|
36
|
-
template_data = self.load(template_file)
|
|
37
|
-
merged_template_data = self._deep_merge(merged_template_data, template_data)
|
|
38
|
-
|
|
39
|
-
return self._deep_merge(merged_template_data, data)
|
|
40
|
-
|
|
41
|
-
def _deep_merge(self, dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
|
|
42
|
-
merged = deepcopy(dict1)
|
|
43
|
-
for key, value in dict2.items():
|
|
44
|
-
if key in merged and isinstance(merged[key], list) and isinstance(value, list):
|
|
45
|
-
# Heuristic to check if these are lists of keywords (dicts with a 'name')
|
|
46
|
-
# This logic is specific to how this project uses YAML inheritance.
|
|
47
|
-
is_keyword_list = all(isinstance(i, dict) and "name" in i for i in value) and all(
|
|
48
|
-
isinstance(i, dict) and "name" in i for i in merged[key]
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
if is_keyword_list:
|
|
52
|
-
merged_by_name = {item["name"]: item for item in merged[key]}
|
|
53
|
-
for item in value:
|
|
54
|
-
if item["name"] in merged_by_name:
|
|
55
|
-
# It's a dict merge, so we can recursively call _deep_merge
|
|
56
|
-
merged_by_name[item["name"]] = self._deep_merge(
|
|
57
|
-
merged_by_name[item["name"]], item
|
|
58
|
-
)
|
|
59
|
-
else:
|
|
60
|
-
merged_by_name[item["name"]] = item
|
|
61
|
-
merged[key] = list(merged_by_name.values())
|
|
62
|
-
else:
|
|
63
|
-
# Fallback for simple lists: concatenate and remove duplicates
|
|
64
|
-
# Note: This is a simple approach and might not be suitable for all list types.
|
|
65
|
-
merged[key].extend([item for item in value if item not in merged[key]])
|
|
66
|
-
|
|
67
|
-
elif key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
|
|
68
|
-
merged[key] = self._deep_merge(merged[key], value)
|
|
69
|
-
else:
|
|
70
|
-
merged[key] = value
|
|
71
|
-
return merged
|
|
1
|
+
# pyre-strict
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class YamlInheritanceLoader:
|
|
10
|
+
def __init__(self, base_path: Optional[Path] = None) -> None:
|
|
11
|
+
self.base_path: Path = base_path or Path(".")
|
|
12
|
+
|
|
13
|
+
def load(self, file_name: str) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Load a YAML file by name relative to base_path and resolve inheritance.
|
|
16
|
+
"""
|
|
17
|
+
file_path = self.base_path / file_name
|
|
18
|
+
if not file_path.exists():
|
|
19
|
+
raise FileNotFoundError(f"YAML file not found: {file_path}")
|
|
20
|
+
|
|
21
|
+
with open(file_path, "r") as f:
|
|
22
|
+
data = yaml.safe_load(f) or {}
|
|
23
|
+
|
|
24
|
+
return self._resolve_inheritance(data)
|
|
25
|
+
|
|
26
|
+
def _resolve_inheritance(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
27
|
+
templates = data.get("study", {}).get("template", [])
|
|
28
|
+
if isinstance(templates, str):
|
|
29
|
+
templates = [templates]
|
|
30
|
+
|
|
31
|
+
if not templates:
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
merged_template_data: Dict[str, Any] = {}
|
|
35
|
+
for template_file in templates:
|
|
36
|
+
template_data = self.load(template_file)
|
|
37
|
+
merged_template_data = self._deep_merge(merged_template_data, template_data)
|
|
38
|
+
|
|
39
|
+
return self._deep_merge(merged_template_data, data)
|
|
40
|
+
|
|
41
|
+
def _deep_merge(self, dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
|
|
42
|
+
merged = deepcopy(dict1)
|
|
43
|
+
for key, value in dict2.items():
|
|
44
|
+
if key in merged and isinstance(merged[key], list) and isinstance(value, list):
|
|
45
|
+
# Heuristic to check if these are lists of keywords (dicts with a 'name')
|
|
46
|
+
# This logic is specific to how this project uses YAML inheritance.
|
|
47
|
+
is_keyword_list = all(isinstance(i, dict) and "name" in i for i in value) and all(
|
|
48
|
+
isinstance(i, dict) and "name" in i for i in merged[key]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if is_keyword_list:
|
|
52
|
+
merged_by_name = {item["name"]: item for item in merged[key]}
|
|
53
|
+
for item in value:
|
|
54
|
+
if item["name"] in merged_by_name:
|
|
55
|
+
# It's a dict merge, so we can recursively call _deep_merge
|
|
56
|
+
merged_by_name[item["name"]] = self._deep_merge(
|
|
57
|
+
merged_by_name[item["name"]], item
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
merged_by_name[item["name"]] = item
|
|
61
|
+
merged[key] = list(merged_by_name.values())
|
|
62
|
+
else:
|
|
63
|
+
# Fallback for simple lists: concatenate and remove duplicates
|
|
64
|
+
# Note: This is a simple approach and might not be suitable for all list types.
|
|
65
|
+
merged[key].extend([item for item in value if item not in merged[key]])
|
|
66
|
+
|
|
67
|
+
elif key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
|
|
68
|
+
merged[key] = self._deep_merge(merged[key], value)
|
|
69
|
+
else:
|
|
70
|
+
merged[key] = value
|
|
71
|
+
return merged
|
csrlite/disposition/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
# Disposition package
|
|
2
|
-
# Import main functions but don't re-export to avoid shadowing
|
|
1
|
+
# Disposition package
|
|
2
|
+
# Import main functions but don't re-export to avoid shadowing
|