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.
Files changed (31) hide show
  1. {flatbread-0.1.0 → flatbread-0.1.2}/PKG-INFO +9 -11
  2. flatbread-0.1.2/flatbread/config.py +133 -0
  3. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/tooling.py +36 -8
  4. {flatbread-0.1.0 → flatbread-0.1.2}/pyproject.toml +1 -1
  5. {flatbread-0.1.0 → flatbread-0.1.2}/readme.md +8 -10
  6. flatbread-0.1.0/flatbread/config.py +0 -43
  7. {flatbread-0.1.0 → flatbread-0.1.2}/.gitignore +0 -0
  8. {flatbread-0.1.0 → flatbread-0.1.2}/environment.yml +0 -0
  9. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/__init__.py +0 -0
  10. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/accessors/dataframe.py +0 -0
  11. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/accessors/index.py +0 -0
  12. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/accessors/series.py +0 -0
  13. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/agg/aggregation.py +0 -0
  14. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/agg/totals.py +0 -0
  15. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/chaining.py +0 -0
  16. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/config/config.defaults.json +0 -0
  17. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/percentages.py +0 -0
  18. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/config.py +0 -0
  19. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/constants.py +0 -0
  20. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/display.py +0 -0
  21. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/tablespec.py +0 -0
  22. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/template.jinja.html +0 -0
  23. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/render/template.py +0 -0
  24. {flatbread-0.1.0 → flatbread-0.1.2}/flatbread/testing/dataframe.py +0 -0
  25. {flatbread-0.1.0 → flatbread-0.1.2}/license.md +0 -0
  26. {flatbread-0.1.0 → flatbread-0.1.2}/tests/__init__.py +0 -0
  27. {flatbread-0.1.0 → flatbread-0.1.2}/tests/aggregate/__init__.py +0 -0
  28. {flatbread-0.1.0 → flatbread-0.1.2}/tests/aggregate/test_percentages.py +0 -0
  29. {flatbread-0.1.0 → flatbread-0.1.2}/tests/aggregate/test_totals.py +0 -0
  30. {flatbread-0.1.0 → flatbread-0.1.2}/tests/test_axes.py +0 -0
  31. {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.0
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 through a chainable API. It makes it easy to create summary tables with totals, subtotals, percentages, and aggregations.
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 in one chain
721
+ # Add totals and percentages
724
722
  result = (
725
- df.pita
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
- item: Any,
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 (Any):
113
- Value to fill the new level with.
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 = [add_value_to_key(key, value, level) for key in target]
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 (Any):
160
- Value to fill the new level with.
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 = [add_value_to_key(key, value, level) for key in target]
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flatbread"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Pandas extension for aggregation and tabular display"
9
9
  readme = "readme.md"
10
10
  requires-python = ">=3.10"
@@ -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 through a chainable API. It makes it easy to create summary tables with totals, subtotals, percentages, and aggregations.
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 in one chain
24
+ # Add totals and percentages
27
25
  result = (
28
- df.pita
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