csrlite 0.2.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/common/rtf.py CHANGED
@@ -1,85 +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)
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 | 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
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
@@ -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
@@ -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