flatbread 0.1.0__tar.gz → 0.1.2__tar.gz
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.
- {flatbread-0.1.0 → flatbread-0.1.2}/PKG-INFO +9 -11
- flatbread-0.1.2/flatbread/config.py +133 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/tooling.py +36 -8
- {flatbread-0.1.0 → flatbread-0.1.2}/pyproject.toml +1 -1
- {flatbread-0.1.0 → flatbread-0.1.2}/readme.md +8 -10
- flatbread-0.1.0/flatbread/config.py +0 -43
- {flatbread-0.1.0 → flatbread-0.1.2}/.gitignore +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/environment.yml +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/__init__.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/accessors/dataframe.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/accessors/index.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/accessors/series.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/agg/aggregation.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/agg/totals.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/chaining.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/config/config.defaults.json +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/percentages.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/config.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/constants.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/display.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/tablespec.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/template.jinja.html +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/template.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/testing/dataframe.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/license.md +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/tests/__init__.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/tests/aggregate/__init__.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/tests/aggregate/test_percentages.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/tests/aggregate/test_totals.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/tests/test_axes.py +0 -0
- {flatbread-0.1.0 → flatbread-0.1.2}/tests/test_levels.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flatbread
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Pandas extension for aggregation and tabular display
|
|
5
5
|
Project-URL: Homepage, https://github.com/lcvriend/flatbread
|
|
6
6
|
Author-email: "L.C. Vriend" <vanboefer@gmail.com>
|
|
@@ -695,20 +695,18 @@ Requires-Dist: jinja2
|
|
|
695
695
|
Requires-Dist: pandas>=2.0.0
|
|
696
696
|
Description-Content-Type: text/markdown
|
|
697
697
|
|
|
698
|
-
Here's a concise README for flatbread:
|
|
699
|
-
|
|
700
|
-
```markdown
|
|
701
698
|
# Flatbread
|
|
702
699
|
|
|
703
|
-
Flatbread is a Python library that extends pandas with tabulation features
|
|
700
|
+
Flatbread is a Python library that extends pandas with tabulation features. It makes it easy to create and display summary tables with totals, subtotals, percentages, and aggregations.
|
|
704
701
|
|
|
705
702
|
Flatbread can be accessed in `DataFrames` and `Series` using the `pita` accessor.
|
|
706
703
|
|
|
704
|
+
It uses the [wc-simple-table](https://github.com/lcvriend/wc-simple-table) dataviewer web component to display tables in a notebook: [check out some examples](https://lcvriend.github.io/wc-simple-table/).
|
|
705
|
+
|
|
707
706
|
## Key Features
|
|
708
707
|
|
|
709
708
|
- Add row and column totals/subtotals to DataFrames and Series
|
|
710
709
|
- Calculate and format percentages with proper rounding
|
|
711
|
-
- Chain operations
|
|
712
710
|
- Preserve data types and index structures
|
|
713
711
|
- Table display in Jupyter notebooks
|
|
714
712
|
|
|
@@ -720,12 +718,12 @@ import flatbread
|
|
|
720
718
|
|
|
721
719
|
df = pd.DataFrame(...)
|
|
722
720
|
|
|
723
|
-
# Add totals and percentages
|
|
721
|
+
# Add totals and percentages
|
|
724
722
|
result = (
|
|
725
|
-
df
|
|
726
|
-
.add_totals() # Add grand totals
|
|
727
|
-
.add_subtotals(level=0) # Add subtotals by first index level
|
|
728
|
-
.add_percentages() # Add percentage columns
|
|
723
|
+
df
|
|
724
|
+
.pita.add_totals() # Add grand totals
|
|
725
|
+
.pita.add_subtotals(level=0) # Add subtotals by first index level
|
|
726
|
+
.pita.add_percentages() # Add percentage columns
|
|
729
727
|
)
|
|
730
728
|
|
|
731
729
|
# Display with interactive viewer
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def deep_merge(base: dict, update: dict) -> dict:
|
|
8
|
+
"""
|
|
9
|
+
Deep merge two dictionaries, preserving structure from both.
|
|
10
|
+
|
|
11
|
+
- When both dicts have a dict at the same key, merge recursively
|
|
12
|
+
- When update has keys not in base, add them
|
|
13
|
+
- When types mismatch (e.g., dict in one, value in other), prefer update
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
base : dict
|
|
18
|
+
Base dictionary to merge into
|
|
19
|
+
update : dict
|
|
20
|
+
Dictionary with values to update base with
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
dict
|
|
25
|
+
Merged dictionary
|
|
26
|
+
"""
|
|
27
|
+
merged = base.copy()
|
|
28
|
+
for key, update_val in update.items():
|
|
29
|
+
if key not in merged:
|
|
30
|
+
# Add keys from update that don't exist in base
|
|
31
|
+
merged[key] = update_val
|
|
32
|
+
elif isinstance(update_val, dict) and isinstance(merged[key], dict):
|
|
33
|
+
# Recursively merge nested dictionaries
|
|
34
|
+
merged[key] = deep_merge(merged[key], update_val)
|
|
35
|
+
else:
|
|
36
|
+
# Override base value with update value
|
|
37
|
+
merged[key] = update_val
|
|
38
|
+
return merged
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_project_config(max_levels: int = 5) -> Path|None:
|
|
42
|
+
"""
|
|
43
|
+
Find project-level .flatbread.json, traversing up from current directory.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
max_levels : int, default 5
|
|
48
|
+
Maximum number of directory levels to traverse upward
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
Path or None
|
|
53
|
+
Path to the config file if found, None otherwise
|
|
54
|
+
"""
|
|
55
|
+
current_dir = Path.cwd()
|
|
56
|
+
home_dir = Path.home()
|
|
57
|
+
|
|
58
|
+
# Check current directory and up to max_levels parent directories
|
|
59
|
+
for _ in range(max_levels + 1):
|
|
60
|
+
config_path = current_dir / ".flatbread.json"
|
|
61
|
+
if config_path.is_file():
|
|
62
|
+
return config_path
|
|
63
|
+
|
|
64
|
+
# Stop if we've reached the filesystem root or home directory
|
|
65
|
+
if current_dir == current_dir.parent or current_dir == home_dir:
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
# Move up one directory
|
|
69
|
+
current_dir = current_dir.parent
|
|
70
|
+
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def read_config():
|
|
75
|
+
"""
|
|
76
|
+
Read configuration with priority: project > user > defaults.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
dict
|
|
81
|
+
Merged configuration dictionary
|
|
82
|
+
"""
|
|
83
|
+
# 1. Look for project config first
|
|
84
|
+
project_config = None
|
|
85
|
+
if project_path := find_project_config():
|
|
86
|
+
project_config = json.loads(project_path.read_text())
|
|
87
|
+
|
|
88
|
+
# 2. Look for user config
|
|
89
|
+
user_config = None
|
|
90
|
+
user_config_path = Path('~/.flatbread.json').expanduser()
|
|
91
|
+
if user_config_path.exists():
|
|
92
|
+
user_config = json.loads(user_config_path.read_text())
|
|
93
|
+
|
|
94
|
+
# 3. Get default config
|
|
95
|
+
package_path = Path(__file__).resolve().parent
|
|
96
|
+
config = json.loads((package_path / 'config/config.defaults.json').read_text())
|
|
97
|
+
|
|
98
|
+
# Merge configs with right precedence
|
|
99
|
+
if user_config:
|
|
100
|
+
config = deep_merge(config, user_config)
|
|
101
|
+
if project_config:
|
|
102
|
+
config = deep_merge(config, project_config)
|
|
103
|
+
|
|
104
|
+
return config
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def inject_defaults(defaults: dict) -> Callable:
|
|
108
|
+
"""
|
|
109
|
+
Load defaults if keywords are None or undefined when calling a function.
|
|
110
|
+
|
|
111
|
+
Arguments
|
|
112
|
+
---------
|
|
113
|
+
defaults (dict):
|
|
114
|
+
Dictionary of keywords and default values.
|
|
115
|
+
|
|
116
|
+
Return
|
|
117
|
+
------
|
|
118
|
+
func:
|
|
119
|
+
Function that will load defaults.
|
|
120
|
+
|
|
121
|
+
Notes
|
|
122
|
+
-----
|
|
123
|
+
This decorator will override any default values set in the function definition.
|
|
124
|
+
"""
|
|
125
|
+
def decorator(func):
|
|
126
|
+
@functools.wraps(func)
|
|
127
|
+
def wrapper(*args, **kwargs):
|
|
128
|
+
for key, val in defaults.items():
|
|
129
|
+
if kwargs.get(key) is None:
|
|
130
|
+
kwargs[key] = val
|
|
131
|
+
return func(*args, **kwargs)
|
|
132
|
+
return wrapper
|
|
133
|
+
return decorator
|
|
@@ -86,7 +86,7 @@ def sort_totals(
|
|
|
86
86
|
@singledispatch
|
|
87
87
|
def add_level(
|
|
88
88
|
data,
|
|
89
|
-
|
|
89
|
+
values: Any|list[Any],
|
|
90
90
|
level: int = 0,
|
|
91
91
|
level_name: Any = None,
|
|
92
92
|
axis: Axis = 0,
|
|
@@ -97,7 +97,7 @@ def add_level(
|
|
|
97
97
|
@add_level.register
|
|
98
98
|
def _(
|
|
99
99
|
data: pd.DataFrame,
|
|
100
|
-
value: Any,
|
|
100
|
+
value: Any|list[Any],
|
|
101
101
|
level: int = 0,
|
|
102
102
|
level_name: Any = None,
|
|
103
103
|
axis: Axis = 0,
|
|
@@ -109,8 +109,8 @@ def _(
|
|
|
109
109
|
----------
|
|
110
110
|
data (pd.DataFrame):
|
|
111
111
|
Input DataFrame.
|
|
112
|
-
value
|
|
113
|
-
|
|
112
|
+
value Any|list[Any]
|
|
113
|
+
Either a single value to fill the entire level with, or a list of values with length matching the axis size. Values will be mapped in order.
|
|
114
114
|
level (int, optional):
|
|
115
115
|
Position to insert the new level. Defaults to 0 (start).
|
|
116
116
|
level_name (Any, optional):
|
|
@@ -126,11 +126,25 @@ def _(
|
|
|
126
126
|
data = data.copy()
|
|
127
127
|
target = data.index if axis in [0, 'index'] else data.columns
|
|
128
128
|
|
|
129
|
+
if isinstance(value, list):
|
|
130
|
+
if len(value) != len(target):
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"Length of values list ({len(value)}) must match "
|
|
133
|
+
f"length of {'index' if axis in [0, 'index'] else 'columns'} ({len(target)})"
|
|
134
|
+
)
|
|
135
|
+
|
|
129
136
|
if not isinstance(target, pd.MultiIndex):
|
|
130
137
|
original_name = target.name
|
|
131
138
|
target = pd.MultiIndex.from_arrays([target], names=[original_name])
|
|
132
139
|
|
|
133
|
-
new_keys = [
|
|
140
|
+
new_keys = [
|
|
141
|
+
add_value_to_key(
|
|
142
|
+
key,
|
|
143
|
+
value[i] if isinstance(value, list) else value,
|
|
144
|
+
level
|
|
145
|
+
)
|
|
146
|
+
for i, key in enumerate(target)
|
|
147
|
+
]
|
|
134
148
|
new_names = add_value_to_key(target.names, level_name, level)
|
|
135
149
|
new_index = pd.MultiIndex.from_tuples(new_keys, names=new_names)
|
|
136
150
|
|
|
@@ -156,8 +170,8 @@ def _(
|
|
|
156
170
|
----------
|
|
157
171
|
data (pd.Series):
|
|
158
172
|
Input Series.
|
|
159
|
-
value
|
|
160
|
-
|
|
173
|
+
value Any|list[Any]
|
|
174
|
+
Either a single value to fill the entire level with, or a list of values with length matching the axis size. Values will be mapped in order.
|
|
161
175
|
level (int, optional):
|
|
162
176
|
Position to insert the new level. Defaults to 0 (start).
|
|
163
177
|
level_name (Any, optional):
|
|
@@ -173,11 +187,25 @@ def _(
|
|
|
173
187
|
data = data.copy()
|
|
174
188
|
target = data.index
|
|
175
189
|
|
|
190
|
+
if isinstance(value, list):
|
|
191
|
+
if len(value) != len(target):
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"Length of values list ({len(value)}) must match "
|
|
194
|
+
f"length of index ({len(target)})"
|
|
195
|
+
)
|
|
196
|
+
|
|
176
197
|
if not isinstance(target, pd.MultiIndex):
|
|
177
198
|
original_name = target.name
|
|
178
199
|
target = pd.MultiIndex.from_arrays([target], names=[original_name])
|
|
179
200
|
|
|
180
|
-
new_keys = [
|
|
201
|
+
new_keys = [
|
|
202
|
+
add_value_to_key(
|
|
203
|
+
key,
|
|
204
|
+
value[i] if isinstance(value, list) else value,
|
|
205
|
+
level
|
|
206
|
+
)
|
|
207
|
+
for i, key in enumerate(target)
|
|
208
|
+
]
|
|
181
209
|
new_names = add_value_to_key(target.names, level_name, level)
|
|
182
210
|
new_index = pd.MultiIndex.from_tuples(new_keys, names=new_names)
|
|
183
211
|
data.index = new_index
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
Here's a concise README for flatbread:
|
|
2
|
-
|
|
3
|
-
```markdown
|
|
4
1
|
# Flatbread
|
|
5
2
|
|
|
6
|
-
Flatbread is a Python library that extends pandas with tabulation features
|
|
3
|
+
Flatbread is a Python library that extends pandas with tabulation features. It makes it easy to create and display summary tables with totals, subtotals, percentages, and aggregations.
|
|
7
4
|
|
|
8
5
|
Flatbread can be accessed in `DataFrames` and `Series` using the `pita` accessor.
|
|
9
6
|
|
|
7
|
+
It uses the [wc-simple-table](https://github.com/lcvriend/wc-simple-table) dataviewer web component to display tables in a notebook: [check out some examples](https://lcvriend.github.io/wc-simple-table/).
|
|
8
|
+
|
|
10
9
|
## Key Features
|
|
11
10
|
|
|
12
11
|
- Add row and column totals/subtotals to DataFrames and Series
|
|
13
12
|
- Calculate and format percentages with proper rounding
|
|
14
|
-
- Chain operations
|
|
15
13
|
- Preserve data types and index structures
|
|
16
14
|
- Table display in Jupyter notebooks
|
|
17
15
|
|
|
@@ -23,12 +21,12 @@ import flatbread
|
|
|
23
21
|
|
|
24
22
|
df = pd.DataFrame(...)
|
|
25
23
|
|
|
26
|
-
# Add totals and percentages
|
|
24
|
+
# Add totals and percentages
|
|
27
25
|
result = (
|
|
28
|
-
df
|
|
29
|
-
.add_totals() # Add grand totals
|
|
30
|
-
.add_subtotals(level=0) # Add subtotals by first index level
|
|
31
|
-
.add_percentages() # Add percentage columns
|
|
26
|
+
df
|
|
27
|
+
.pita.add_totals() # Add grand totals
|
|
28
|
+
.pita.add_subtotals(level=0) # Add subtotals by first index level
|
|
29
|
+
.pita.add_percentages() # Add percentage columns
|
|
32
30
|
)
|
|
33
31
|
|
|
34
32
|
# Display with interactive viewer
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import json
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Callable
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def read_config():
|
|
8
|
-
config_path = Path('~/.flatbread/config.json').expanduser()
|
|
9
|
-
if not config_path.exists():
|
|
10
|
-
package_path = Path(__file__).resolve().parent
|
|
11
|
-
config_path = package_path / 'config/config.defaults.json'
|
|
12
|
-
json_string = config_path.read_text()
|
|
13
|
-
config = json.loads(json_string)
|
|
14
|
-
return config
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def inject_defaults(defaults: dict) -> Callable:
|
|
18
|
-
"""
|
|
19
|
-
Load defaults if keywords are None or undefined when calling a function.
|
|
20
|
-
|
|
21
|
-
Arguments
|
|
22
|
-
---------
|
|
23
|
-
defaults (dict):
|
|
24
|
-
Dictionary of keywords and default values.
|
|
25
|
-
|
|
26
|
-
Return
|
|
27
|
-
------
|
|
28
|
-
func:
|
|
29
|
-
Function that will load defaults.
|
|
30
|
-
|
|
31
|
-
Notes
|
|
32
|
-
-----
|
|
33
|
-
This decorator will override any default values set in the function definition.
|
|
34
|
-
"""
|
|
35
|
-
def decorator(func):
|
|
36
|
-
@functools.wraps(func)
|
|
37
|
-
def wrapper(*args, **kwargs):
|
|
38
|
-
for key, val in defaults.items():
|
|
39
|
-
if kwargs.get(key) is None:
|
|
40
|
-
kwargs[key] = val
|
|
41
|
-
return func(*args, **kwargs)
|
|
42
|
-
return wrapper
|
|
43
|
-
return decorator
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|