flatbread 0.1.2__tar.gz → 0.1.4__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 (30) hide show
  1. {flatbread-0.1.2 → flatbread-0.1.4}/PKG-INFO +1 -1
  2. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/accessors/dataframe.py +4 -0
  3. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/accessors/series.py +4 -0
  4. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/agg/aggregation.py +18 -1
  5. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/agg/totals.py +6 -0
  6. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/chaining.py +24 -4
  7. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/config/config.defaults.json +11 -1
  8. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/render/constants.py +16 -3
  9. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/render/display.py +46 -0
  10. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/render/tablespec.py +43 -5
  11. {flatbread-0.1.2 → flatbread-0.1.4}/pyproject.toml +1 -1
  12. {flatbread-0.1.2 → flatbread-0.1.4}/.gitignore +0 -0
  13. {flatbread-0.1.2 → flatbread-0.1.4}/environment.yml +0 -0
  14. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/__init__.py +0 -0
  15. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/accessors/index.py +0 -0
  16. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/config.py +0 -0
  17. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/percentages.py +0 -0
  18. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/render/config.py +0 -0
  19. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/render/template.jinja.html +0 -0
  20. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/render/template.py +0 -0
  21. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/testing/dataframe.py +0 -0
  22. {flatbread-0.1.2 → flatbread-0.1.4}/flatbread/tooling.py +0 -0
  23. {flatbread-0.1.2 → flatbread-0.1.4}/license.md +0 -0
  24. {flatbread-0.1.2 → flatbread-0.1.4}/readme.md +0 -0
  25. {flatbread-0.1.2 → flatbread-0.1.4}/tests/__init__.py +0 -0
  26. {flatbread-0.1.2 → flatbread-0.1.4}/tests/aggregate/__init__.py +0 -0
  27. {flatbread-0.1.2 → flatbread-0.1.4}/tests/aggregate/test_percentages.py +0 -0
  28. {flatbread-0.1.2 → flatbread-0.1.4}/tests/aggregate/test_totals.py +0 -0
  29. {flatbread-0.1.2 → flatbread-0.1.4}/tests/test_axes.py +0 -0
  30. {flatbread-0.1.2 → flatbread-0.1.4}/tests/test_levels.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatbread
3
- Version: 0.1.2
3
+ Version: 0.1.4
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>
@@ -65,6 +65,7 @@ class PitaFrame(PitaDisplayMixin):
65
65
  axis: int = 0,
66
66
  level: int|str|list[int|str] = 0,
67
67
  label: str = None,
68
+ include_level_name: bool = False,
68
69
  ignore_keys: str|list[str]|None = None,
69
70
  _fill: str = '',
70
71
  ) -> pd.DataFrame:
@@ -99,6 +100,7 @@ class PitaFrame(PitaDisplayMixin):
99
100
  axis = axis,
100
101
  level = level,
101
102
  label = label,
103
+ include_level_name = include_level_name,
102
104
  ignore_keys = ignore_keys,
103
105
  _fill = _fill,
104
106
  )
@@ -252,6 +254,7 @@ class PitaFrame(PitaDisplayMixin):
252
254
  axis: int = 2,
253
255
  level: int|str|list[int|str] = 0,
254
256
  label: str|None = None,
257
+ include_level_name: bool = False,
255
258
  ignore_keys: str|list[str]|None = None,
256
259
  _fill: str = '',
257
260
  ) -> pd.DataFrame:
@@ -279,6 +282,7 @@ class PitaFrame(PitaDisplayMixin):
279
282
  axis = axis,
280
283
  level = level,
281
284
  label = label,
285
+ include_level_name = include_level_name,
282
286
  ignore_keys = ignore_keys,
283
287
  _fill = _fill,
284
288
  )
@@ -60,6 +60,7 @@ class PitaSeries(PitaDisplayMixin):
60
60
  aggfunc: str|Callable,
61
61
  level: int|str|list[int|str] = 0,
62
62
  label: str = None,
63
+ include_level_name: bool = False,
63
64
  ignore_keys: str|list[str]|None = None,
64
65
  _fill: str = '',
65
66
  ) -> pd.Series:
@@ -91,6 +92,7 @@ class PitaSeries(PitaDisplayMixin):
91
92
  aggfunc,
92
93
  level = level,
93
94
  label = label,
95
+ include_level_name = include_level_name,
94
96
  ignore_keys = ignore_keys,
95
97
  _fill = _fill,
96
98
  )
@@ -256,6 +258,7 @@ class PitaSeries(PitaDisplayMixin):
256
258
  self,
257
259
  level: int|str|list[int|str] = 0,
258
260
  label: str|None = None,
261
+ include_level_name: bool = False,
259
262
  ignore_keys: str|list[str]|None = None,
260
263
  _fill: str = '',
261
264
  ) -> pd.Series:
@@ -280,6 +283,7 @@ class PitaSeries(PitaDisplayMixin):
280
283
  self._obj,
281
284
  level = level,
282
285
  label = label,
286
+ include_level_name = include_level_name,
283
287
  ignore_keys = ignore_keys,
284
288
  _fill = _fill,
285
289
  )
@@ -109,6 +109,7 @@ def add_subagg(
109
109
  *args,
110
110
  level: int|str|list[int|str] = 0,
111
111
  label: str|None = None,
112
+ include_level_name: bool = False,
112
113
  ignore_keys: str|list[str]|None = None,
113
114
  **kwargs,
114
115
  ):
@@ -123,6 +124,7 @@ def _(
123
124
  *args,
124
125
  level: int|str|list[int|str] = 0,
125
126
  label: str|None = None,
127
+ include_level_name: bool = False,
126
128
  ignore_keys: str|list[str]|None = None,
127
129
  _fill = '',
128
130
  **kwargs,
@@ -134,6 +136,7 @@ def _(
134
136
  *args,
135
137
  level = level,
136
138
  label = label,
139
+ include_level_name = include_level_name,
137
140
  ignore_keys = ignore_keys,
138
141
  _fill = _fill,
139
142
  **kwargs,
@@ -150,6 +153,7 @@ def _(
150
153
  axis: int = 0,
151
154
  level: int|str|list[int|str] = 0,
152
155
  label: str|None = None,
156
+ include_level_name: bool = False,
153
157
  ignore_keys: str|list[str]|None = None,
154
158
  _fill = '',
155
159
  **kwargs,
@@ -161,6 +165,7 @@ def _(
161
165
  *args,
162
166
  level = level,
163
167
  label = label,
168
+ include_level_name = include_level_name,
164
169
  ignore_keys = ignore_keys,
165
170
  _fill = _fill,
166
171
  **kwargs,
@@ -176,6 +181,7 @@ def _subagg_implementation(
176
181
  *args,
177
182
  level: int|str|list[int|str] = 0,
178
183
  label: str|None = None,
184
+ include_level_name: bool = False,
179
185
  ignore_keys: str|list[str]|None = None,
180
186
  _fill = '',
181
187
  **kwargs,
@@ -196,8 +202,19 @@ def _subagg_implementation(
196
202
  for levels, group in groups:
197
203
  # create key
198
204
  levels = (levels,) if pd.api.types.is_scalar(levels) else levels
205
+
206
+ # format label with level name if requested
207
+ current_label = label
208
+ if include_level_name:
209
+ # get level name
210
+ if isinstance(level, (int, str)):
211
+ level_name = names[level] if isinstance(level, int) else level
212
+ current_label = f"{label} {level_name}"
213
+ elif isinstance(levels[-1], str):
214
+ current_label = f"{label} {levels[-1]}"
215
+
199
216
  padding = [_fill] * (len(names) - len(levels) - 1)
200
- key = list(levels) + [label] + padding
217
+ key = list(levels) + [current_label] + padding
201
218
 
202
219
  # ignore totals and subtotal rows when aggregating
203
220
  rows = chaining.get_data_mask(group.index, ignore_keys)
@@ -95,6 +95,7 @@ def add_subtotals(
95
95
  def _(
96
96
  data: pd.Series,
97
97
  level: int|str|list[int|str] = 0,
98
+ include_level_name: bool = False,
98
99
  label: str = 'Subtotals',
99
100
  ignore_keys: str|list[str]|None = 'Totals',
100
101
  _fill: str|None = '',
@@ -104,6 +105,7 @@ def _(
104
105
  'sum',
105
106
  level = level,
106
107
  label = label,
108
+ include_level_name = include_level_name,
107
109
  ignore_keys = ignore_keys,
108
110
  _fill = _fill,
109
111
  )
@@ -118,6 +120,7 @@ def _(
118
120
  axis: int = 0,
119
121
  level: int|str|list[int|str] = 0,
120
122
  label: str = 'Subtotals',
123
+ include_level_name: bool = False,
121
124
  ignore_keys: str|list[str]|None = 'Totals',
122
125
  _fill: str = '',
123
126
  ) -> pd.DataFrame:
@@ -128,6 +131,7 @@ def _(
128
131
  axis = axis,
129
132
  level = level,
130
133
  label = label,
134
+ include_level_name = include_level_name,
131
135
  ignore_keys = ignore_keys,
132
136
  _fill = _fill,
133
137
  )
@@ -139,6 +143,7 @@ def _(
139
143
  axis = 0,
140
144
  level = level,
141
145
  label = label,
146
+ include_level_name = include_level_name,
142
147
  ignore_keys = ignore_keys,
143
148
  _fill = _fill,
144
149
  )
@@ -147,6 +152,7 @@ def _(
147
152
  axis = 1,
148
153
  level = level,
149
154
  label = label,
155
+ include_level_name = include_level_name,
150
156
  ignore_keys = ignore_keys,
151
157
  _fill = _fill,
152
158
  )
@@ -13,18 +13,38 @@ def get_data_mask(index, ignore_keys):
13
13
  index (pd.Index):
14
14
  The index used for determining if a row/column contains data or not.
15
15
  ignore_keys (list[str]):
16
- List of index keys indicating that a row/column is *not* a data column. If the index is a MultiIndex then a row/column will be ignored if the key is in the keys of the index, else a row/column will be ignored if it is equal to the key in the index.
16
+ List of index keys indicating that a row/column is *not* a data column. If the index is a MultiIndex then a row/column will be ignored if the key is in the keys of the index, else a row/column will be ignored if it is equal to or a prefix of the key in the index.
17
17
 
18
18
  Returns
19
19
  -------
20
20
  pd.Index:
21
21
  Boolean index indicating which rows/columns refer to data.
22
22
  """
23
+ if ignore_keys is None:
24
+ return pd.Series(True, index=index)
25
+
26
+ # Convert single string to list
27
+ if isinstance(ignore_keys, str):
28
+ ignore_keys = [ignore_keys]
29
+
30
+ def should_keep(value):
31
+ # direct match
32
+ if value in ignore_keys:
33
+ return False
34
+
35
+ # check for prefix
36
+ if isinstance(value, str):
37
+ for key in ignore_keys:
38
+ if isinstance(key, str) and value.startswith(key):
39
+ return False
40
+ return True
41
+
23
42
  if isinstance(index, pd.MultiIndex):
24
- ignored = index.map(lambda i: all(key not in i for key in ignore_keys))
43
+ result = [all(should_keep(el) for el in idx) for idx in index]
25
44
  else:
26
- ignored = index.map(lambda i: all(key != i for key in ignore_keys))
27
- return ignored
45
+ result = [should_keep(idx) for idx in index]
46
+
47
+ return pd.Series(result, index=index)
28
48
 
29
49
 
30
50
  def persist_ignored(component: str, label: str) -> Callable:
@@ -5,6 +5,7 @@
5
5
  },
6
6
  "subtotals": {
7
7
  "label": "Subtotals",
8
+ "include_level_name": false,
8
9
  "ignore_keys": ["Totals"]
9
10
  },
10
11
  "percentages": {
@@ -13,5 +14,14 @@
13
14
  "ndigits": -1,
14
15
  "base": 1
15
16
  },
16
- "locale": null
17
+ "locale": null,
18
+ "format_presets": {
19
+ "currency_eur": {
20
+ "dtypes": ["float", "int"],
21
+ "options": {
22
+ "style": "currency",
23
+ "currency": "EUR"
24
+ }
25
+ }
26
+ }
17
27
  }
@@ -2,6 +2,19 @@
2
2
  from flatbread import DEFAULTS
3
3
 
4
4
 
5
+ USER_PRESETS = DEFAULTS.get("format_presets", {})
6
+ USER_PRESETS_BY_DTYPE = {
7
+ dtype: set()
8
+ for dtype in ["float", "int", "datetime", "str", "category"]
9
+ }
10
+
11
+ for preset_name, preset_config in USER_PRESETS.items():
12
+ preset_dtypes = preset_config.get("dtypes", ["float", "int"]) # Default to numeric
13
+ for dtype in preset_dtypes:
14
+ if dtype in USER_PRESETS_BY_DTYPE:
15
+ USER_PRESETS_BY_DTYPE[dtype].add(preset_name)
16
+
17
+
5
18
  SMART_FORMATS = {
6
19
  'percentages': {
7
20
  'labels': [DEFAULTS['percentages']['label_pct']],
@@ -43,7 +56,7 @@ NUMBER_PRESETS = {"default", "currency", "percentage", "compact", "diffs"}
43
56
  DATE_PRESETS = {"default", "date", "datetime"}
44
57
 
45
58
  DTYPE_TO_PRESETS = {
46
- "float": NUMBER_PRESETS,
47
- "int": NUMBER_PRESETS,
48
- "datetime": DATE_PRESETS
59
+ "float": NUMBER_PRESETS.union(USER_PRESETS_BY_DTYPE["float"]),
60
+ "int": NUMBER_PRESETS.union(USER_PRESETS_BY_DTYPE["int"]),
61
+ "datetime": DATE_PRESETS.union(USER_PRESETS_BY_DTYPE["datetime"]),
49
62
  }
@@ -147,6 +147,52 @@ class PitaDisplayMixin:
147
147
  self._table_spec_builder.set_formats(formats)
148
148
  return self
149
149
 
150
+ def list_format_presets(self, dtype: str = None) -> dict[str, dict]:
151
+ """List available format presets, optionally filtered by data type
152
+
153
+ Parameters
154
+ ----------
155
+ dtype : str, optional
156
+ Filter presets by data type compatibility. Options: 'float', 'int',
157
+ 'datetime', 'str', 'category'.
158
+
159
+ Returns
160
+ -------
161
+ dict
162
+ Dictionary of preset names and their configurations
163
+ """
164
+ from flatbread.render.constants import USER_PRESETS, DTYPE_TO_PRESETS
165
+
166
+ # Define built-in presets with their configurations
167
+ built_in = {
168
+ "default": {"notation": "standard"},
169
+ "currency": {"style": "currency", "currency": "USD"},
170
+ "percentage": {"style": "percent"},
171
+ "compact": {"notation": "compact"},
172
+ "date": {"dateStyle": "short"},
173
+ "datetime": {"dateStyle": "short", "timeStyle": "short"}
174
+ }
175
+
176
+ # Extract user preset options
177
+ user_options = {
178
+ name: config.get("options", {})
179
+ for name, config in USER_PRESETS.items()
180
+ }
181
+
182
+ # Combine built-in and user presets
183
+ all_presets = {**built_in, **user_options}
184
+
185
+ # Filter by dtype if specified
186
+ if dtype:
187
+ if dtype not in DTYPE_TO_PRESETS:
188
+ valid_dtypes = ", ".join(sorted(DTYPE_TO_PRESETS.keys()))
189
+ raise ValueError(f"Invalid dtype '{dtype}'. Valid options are: {valid_dtypes}")
190
+
191
+ valid_presets = DTYPE_TO_PRESETS.get(dtype, set())
192
+ return {k: v for k, v in all_presets.items() if k in valid_presets}
193
+
194
+ return all_presets
195
+
150
196
  def _repr_html_(self) -> str:
151
197
  """Generate HTML representation for Jupyter display"""
152
198
  spec = self._table_spec_builder.get_spec_as_json()
@@ -94,17 +94,51 @@ class TableSpecBuilder:
94
94
  return format_type['options']
95
95
  return None
96
96
 
97
- def set_format(self, column: str, format_spec: ColumnFormat) -> None:
97
+ def set_format(self, column: str, format_spec: str | dict[str, Any]) -> None:
98
+ """Set format options for a column
99
+
100
+ Parameters
101
+ ----------
102
+ column : str
103
+ Column to format
104
+ format_spec : str | dict
105
+ Either a preset name (e.g. 'currency') or format options dict
106
+ """
107
+ from flatbread.render.constants import USER_PRESETS, DTYPE_TO_PRESETS, DEFAULT_DTYPES
108
+
98
109
  if isinstance(format_spec, str):
110
+ # Check if it's a user-defined preset
111
+ if format_spec in USER_PRESETS:
112
+ pandas_dtype = str(self._data[column].dtype)
113
+ simple_dtype = DEFAULT_DTYPES.get(pandas_dtype, 'str')
114
+
115
+ # Get allowed dtypes for this preset
116
+ preset_config = USER_PRESETS[format_spec]
117
+ allowed_dtypes = preset_config.get("dtypes", ["float", "int"])
118
+
119
+ if simple_dtype in allowed_dtypes:
120
+ self._format_options[column] = preset_config.get("options", {})
121
+ return
122
+ else:
123
+ raise ValueError(
124
+ f"Preset '{format_spec}' is not compatible with column '{column}' "
125
+ f"of dtype {pandas_dtype} (mapped to {simple_dtype}). "
126
+ f"This preset supports: {', '.join(allowed_dtypes)}"
127
+ )
128
+
129
+ # Handle built-in presets
99
130
  pandas_dtype = str(self._data[column].dtype)
100
131
  simple_dtype = DEFAULT_DTYPES.get(pandas_dtype, 'str')
101
132
  valid_presets = DTYPE_TO_PRESETS.get(simple_dtype, set())
133
+
102
134
  if format_spec not in valid_presets:
103
135
  valid = ", ".join(sorted(valid_presets))
104
136
  raise ValueError(
105
137
  f"Invalid preset '{format_spec}' for dtype {pandas_dtype} "
106
138
  f"(mapped to {simple_dtype}). Valid presets are: {valid}"
107
139
  )
140
+
141
+ # If we reached here, either format_spec is a dict or a valid built-in preset
108
142
  self._format_options[column] = format_spec
109
143
 
110
144
  def set_formats(self, formats: FormatSpec) -> None:
@@ -112,11 +146,15 @@ class TableSpecBuilder:
112
146
 
113
147
  Parameters
114
148
  ----------
115
- formats : dict, list or callable
116
- Either dict mapping column names to format specs,
117
- a list of length columns,
118
- or function that takes DataFrame and returns such dict
149
+ formats : str, dict, list or callable
150
+ - If string: apply the same format preset to all columns
151
+ - If dict: mapping column names to format specs
152
+ - If list: format specs in same order as columns
153
+ - If callable: function that takes DataFrame and returns a dict
119
154
  """
155
+ if isinstance(formats, str):
156
+ formats = {column: formats for column in self._data.columns}
157
+
120
158
  if callable(formats):
121
159
  formats = formats(self._data)
122
160
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flatbread"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Pandas extension for aggregation and tabular display"
9
9
  readme = "readme.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes