flatbread 0.1.4__tar.gz → 0.2.0__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 (55) hide show
  1. flatbread-0.2.0/.gitignore +10 -0
  2. {flatbread-0.1.4 → flatbread-0.2.0}/PKG-INFO +5 -2
  3. flatbread-0.2.0/flatbread/__init__.py +17 -0
  4. {flatbread-0.1.4 → flatbread-0.2.0}/flatbread/accessors/dataframe.py +113 -32
  5. {flatbread-0.1.4 → flatbread-0.2.0}/flatbread/accessors/series.py +114 -29
  6. flatbread-0.2.0/flatbread/axes.py +368 -0
  7. flatbread-0.2.0/flatbread/chaining.py +141 -0
  8. flatbread-0.2.0/flatbread/config/__init__.py +4 -0
  9. flatbread-0.2.0/flatbread/config/config.defaults.json +82 -0
  10. flatbread-0.2.0/flatbread/config/service.py +144 -0
  11. flatbread-0.2.0/flatbread/io/excel.py +209 -0
  12. flatbread-0.2.0/flatbread/output/excel/__init__.py +1 -0
  13. flatbread-0.2.0/flatbread/output/excel/excel.py +209 -0
  14. flatbread-0.2.0/flatbread/output/formats.py +102 -0
  15. flatbread-0.2.0/flatbread/output/html/__init__.py +2 -0
  16. flatbread-0.2.0/flatbread/output/html/constants.py +5 -0
  17. flatbread-0.2.0/flatbread/output/html/display.py +310 -0
  18. flatbread-0.2.0/flatbread/output/html/tablespec.py +265 -0
  19. {flatbread-0.1.4/flatbread/render → flatbread-0.2.0/flatbread/output/html/templates}/template.jinja.html +6 -4
  20. flatbread-0.2.0/flatbread/testing/dataframe.py +162 -0
  21. flatbread-0.2.0/flatbread/tooling.py +156 -0
  22. flatbread-0.2.0/flatbread/transforms/aggregation.py +209 -0
  23. flatbread-0.2.0/flatbread/transforms/percentages.py +419 -0
  24. flatbread-0.2.0/flatbread/transforms/totals.py +262 -0
  25. flatbread-0.2.0/flatbread/types.py +4 -0
  26. {flatbread-0.1.4 → flatbread-0.2.0}/pyproject.toml +5 -2
  27. flatbread-0.2.0/tests/transforms/__init__.py +0 -0
  28. flatbread-0.2.0/tests/transforms/test_percentages.py +223 -0
  29. flatbread-0.2.0/tests/transforms/test_totals.py +204 -0
  30. flatbread-0.2.0/uv.lock +2093 -0
  31. flatbread-0.1.4/.gitignore +0 -5
  32. flatbread-0.1.4/flatbread/__init__.py +0 -7
  33. flatbread-0.1.4/flatbread/agg/aggregation.py +0 -237
  34. flatbread-0.1.4/flatbread/agg/totals.py +0 -171
  35. flatbread-0.1.4/flatbread/chaining.py +0 -116
  36. flatbread-0.1.4/flatbread/config/config.defaults.json +0 -27
  37. flatbread-0.1.4/flatbread/config.py +0 -133
  38. flatbread-0.1.4/flatbread/percentages.py +0 -224
  39. flatbread-0.1.4/flatbread/render/config.py +0 -69
  40. flatbread-0.1.4/flatbread/render/constants.py +0 -62
  41. flatbread-0.1.4/flatbread/render/display.py +0 -231
  42. flatbread-0.1.4/flatbread/render/tablespec.py +0 -191
  43. flatbread-0.1.4/flatbread/render/template.py +0 -24
  44. flatbread-0.1.4/flatbread/tooling.py +0 -244
  45. flatbread-0.1.4/tests/aggregate/test_percentages.py +0 -45
  46. flatbread-0.1.4/tests/aggregate/test_totals.py +0 -159
  47. {flatbread-0.1.4 → flatbread-0.2.0}/environment.yml +0 -0
  48. {flatbread-0.1.4 → flatbread-0.2.0}/flatbread/accessors/index.py +0 -0
  49. {flatbread-0.1.4/tests → flatbread-0.2.0/flatbread/output}/__init__.py +0 -0
  50. /flatbread-0.1.4/flatbread/testing/dataframe.py → /flatbread-0.2.0/flatbread/testing/.ipynb_checkpoints/dataframe-checkpoint.py +0 -0
  51. {flatbread-0.1.4 → flatbread-0.2.0}/license.md +0 -0
  52. {flatbread-0.1.4 → flatbread-0.2.0}/readme.md +0 -0
  53. {flatbread-0.1.4/tests/aggregate → flatbread-0.2.0/tests}/__init__.py +0 -0
  54. {flatbread-0.1.4 → flatbread-0.2.0}/tests/test_axes.py +0 -0
  55. {flatbread-0.1.4 → flatbread-0.2.0}/tests/test_levels.py +0 -0
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ *.pyc
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .vscode
10
+ notebooks
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatbread
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: Pandas extension for aggregation and tabular display
5
- Project-URL: Homepage, https://github.com/lcvriend/flatbread
5
+ Project-URL: Homepage, https://github.com/flatbread-dataframes/flatbread
6
6
  Author-email: "L.C. Vriend" <vanboefer@gmail.com>
7
7
  License: # GNU GENERAL PUBLIC LICENSE
8
8
 
@@ -693,6 +693,9 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
693
693
  Requires-Python: >=3.10
694
694
  Requires-Dist: jinja2
695
695
  Requires-Dist: pandas>=2.0.0
696
+ Provides-Extra: dev
697
+ Requires-Dist: ipykernel; extra == 'dev'
698
+ Requires-Dist: jupyter; extra == 'dev'
696
699
  Description-Content-Type: text/markdown
697
700
 
698
701
  # Flatbread
@@ -0,0 +1,17 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ import pandas as pd
5
+ from flatbread.accessors.dataframe import PitaFrame
6
+ from flatbread.accessors.series import PitaSeries
7
+
8
+ class DataFrame(pd.DataFrame):
9
+ pita: PitaFrame
10
+
11
+ class Series(pd.Series):
12
+ pita: PitaSeries
13
+
14
+ from flatbread.config import DEFAULTS
15
+ import flatbread.accessors.dataframe
16
+ import flatbread.accessors.series
17
+ import flatbread.accessors.index
@@ -1,12 +1,14 @@
1
1
  from typing import Any, Callable
2
+ from pathlib import Path
2
3
 
3
4
  import pandas as pd
4
5
 
5
- import flatbread.percentages as pct
6
- import flatbread.agg.aggregation as agg
7
- import flatbread.agg.totals as tot
8
- import flatbread.tooling as tool
9
- from flatbread.render.display import PitaDisplayMixin
6
+ import flatbread.transforms.percentages as pct
7
+ import flatbread.transforms.aggregation as agg
8
+ import flatbread.transforms.totals as totals
9
+ import flatbread.axes as axes
10
+ from flatbread.types import Axis, Level
11
+ from flatbread.output.html import PitaDisplayMixin
10
12
 
11
13
 
12
14
  @pd.api.extensions.register_dataframe_accessor("pita")
@@ -19,8 +21,8 @@ class PitaFrame(PitaDisplayMixin):
19
21
  self,
20
22
  aggfunc: str|Callable,
21
23
  *args,
22
- axis: int = 0,
23
- label: str = None,
24
+ axis: Axis = 0,
25
+ label: str|None = None,
24
26
  ignore_keys: str|list[str]|None = None,
25
27
  _fill: str = '',
26
28
  **kwargs,
@@ -32,7 +34,7 @@ class PitaFrame(PitaDisplayMixin):
32
34
  ----------
33
35
  aggfunc (str|Callable):
34
36
  Function to use for aggregating the data.
35
- axis (int):
37
+ axis (int | Literal["index", "columns", "both"]):
36
38
  Axis to aggregate. Default 0.
37
39
  label (str|None):
38
40
  Label for the aggregation row/column. Default None.
@@ -62,11 +64,12 @@ class PitaFrame(PitaDisplayMixin):
62
64
  def add_subagg(
63
65
  self,
64
66
  aggfunc: str|Callable,
65
- axis: int = 0,
67
+ axis: Axis = 0,
66
68
  level: int|str|list[int|str] = 0,
67
- label: str = None,
69
+ label: str|None = None,
68
70
  include_level_name: bool = False,
69
71
  ignore_keys: str|list[str]|None = None,
72
+ skip_single_rows: bool = True,
70
73
  _fill: str = '',
71
74
  ) -> pd.DataFrame:
72
75
  """
@@ -76,14 +79,18 @@ class PitaFrame(PitaDisplayMixin):
76
79
  ----------
77
80
  aggfunc (str|Callable):
78
81
  Function to use for aggregating the data.
79
- axis (int):
82
+ axis (int | Literal["index", "columns", "both"]):
80
83
  Axis to aggregate. Default 0.
81
84
  levels (int|str|list[int|str]):
82
85
  Levels to aggregate. Default 0.
83
86
  label (str|None):
84
87
  Label for the aggregation row/column. Default None.
88
+ include_level_name (bool):
89
+ Whether to add level name to subtotal label.
85
90
  ignore_keys (str|list[str]|None):
86
- Keys of rows to ignore when aggregating.
91
+ Keys of rows to ignore when aggregating. Default 'Totals'
92
+ skip_single_rows (bool):
93
+ Whether to skip single rows when aggregating. Default True.
87
94
  *args:
88
95
  Positional arguments to pass to func.
89
96
  **kwargs:
@@ -102,18 +109,19 @@ class PitaFrame(PitaDisplayMixin):
102
109
  label = label,
103
110
  include_level_name = include_level_name,
104
111
  ignore_keys = ignore_keys,
112
+ skip_single_rows = skip_single_rows,
105
113
  _fill = _fill,
106
114
  )
107
115
 
108
116
  #region percentages
109
117
  def as_percentages(
110
118
  self,
111
- axis: int = 2,
119
+ axis: Axis = 2,
112
120
  label_totals: str|None = None,
113
121
  ignore_keys: str|list[str]|None = None,
114
122
  ndigits: int|None = None,
115
123
  base: int = 1,
116
- apportioned_rounding: bool = True,
124
+ apportioned_rounding: bool|None = None,
117
125
  ) -> pd.DataFrame:
118
126
  """
119
127
  Transform data to percentages based on specified axis.
@@ -122,7 +130,7 @@ class PitaFrame(PitaDisplayMixin):
122
130
  ----------
123
131
  data (pd.DataFrame):
124
132
  The input DataFrame.
125
- axis (int):
133
+ axis (int | Literal["index", "columns", "both"]):
126
134
  The axis along which percentages are calculated. Percentages are based on:
127
135
  - when axis is 2 then grand total
128
136
  - when axis is 1 then column totals
@@ -157,14 +165,14 @@ class PitaFrame(PitaDisplayMixin):
157
165
 
158
166
  def add_percentages(
159
167
  self,
160
- axis: int = 2,
168
+ axis: Axis = 2,
161
169
  label_n: str|None = None,
162
170
  label_pct: str|None = None,
163
171
  label_totals: str|None = None,
164
172
  ignore_keys: str|list[str]|None = None,
165
173
  ndigits: int|None = None,
166
174
  base: int = 1,
167
- apportioned_rounding: bool = True,
175
+ apportioned_rounding: bool|None = None,
168
176
  interleaf: bool = False,
169
177
  ) -> pd.DataFrame:
170
178
  """
@@ -174,7 +182,7 @@ class PitaFrame(PitaDisplayMixin):
174
182
  ----------
175
183
  data (pd.DataFrame):
176
184
  The input DataFrame.
177
- axis (int):
185
+ axis (int | Literal["index", "columns", "both"]):
178
186
  The axis along which percentages are calculated. Percentages are based on:
179
187
  - when axis is 2 then grand total
180
188
  - when axis is 1 then row totals
@@ -219,7 +227,7 @@ class PitaFrame(PitaDisplayMixin):
219
227
  #region totals
220
228
  def add_totals(
221
229
  self,
222
- axis: int = 2,
230
+ axis: Axis = 2,
223
231
  label: str|None = None,
224
232
  ignore_keys: str|list[str]|None = None,
225
233
  _fill: str = '',
@@ -229,7 +237,7 @@ class PitaFrame(PitaDisplayMixin):
229
237
 
230
238
  Parameters
231
239
  ----------
232
- axis (int):
240
+ axis (int | Literal["index", "columns", "both"]):
233
241
  Axis to sum. If axis == 2 then add totals to both rows and columns. Default 2.
234
242
  label (str|None):
235
243
  Label for the totals row/column. Default 'Totals'.
@@ -241,7 +249,7 @@ class PitaFrame(PitaDisplayMixin):
241
249
  pd.DataFrame:
242
250
  Table with total rows/columns added.
243
251
  """
244
- return tot.add_totals(
252
+ return totals.add_totals( # type: ignore
245
253
  self._obj,
246
254
  axis = axis,
247
255
  label = label,
@@ -251,11 +259,12 @@ class PitaFrame(PitaDisplayMixin):
251
259
 
252
260
  def add_subtotals(
253
261
  self,
254
- axis: int = 2,
262
+ axis: Axis = 2,
255
263
  level: int|str|list[int|str] = 0,
256
264
  label: str|None = None,
257
265
  include_level_name: bool = False,
258
266
  ignore_keys: str|list[str]|None = None,
267
+ skip_single_rows: bool = True,
259
268
  _fill: str = '',
260
269
  ) -> pd.DataFrame:
261
270
  """
@@ -263,47 +272,119 @@ class PitaFrame(PitaDisplayMixin):
263
272
 
264
273
  Parameters
265
274
  ----------
266
- axis (int):
275
+ axis (int | Literal["index", "columns", "both"]):
267
276
  Axis to sum. If axis == 2 then add totals to both rows and columns. Default 2.
268
277
  levels (int|str|list[int|str]):
269
278
  Levels to sum with func. Default 0.
270
279
  label (str|None):
271
280
  Label for the subtotals row/column. Default 'Subtotals'.
281
+ include_level_name (bool):
282
+ Whether to add level name to subtotal label.
272
283
  ignore_keys (str|list[str]|None):
273
284
  Keys of rows to ignore when aggregating. Default 'Totals'
285
+ skip_single_rows (bool):
286
+ Whether to skip single rows when aggregating. Default True.
274
287
 
275
288
  Returns
276
289
  -------
277
290
  pd.DataFrame:
278
291
  Table with total rows/columns added.
279
292
  """
280
- return tot.add_subtotals(
293
+ return totals.add_subtotals( # type: ignore
281
294
  self._obj,
282
295
  axis = axis,
283
296
  level = level,
284
297
  label = label,
285
298
  include_level_name = include_level_name,
286
299
  ignore_keys = ignore_keys,
300
+ skip_single_rows = skip_single_rows,
287
301
  _fill = _fill,
288
302
  )
289
303
 
290
304
  def sort_totals(
291
305
  self,
292
- axis: int = 0,
293
- level: int = 0,
294
- **kwargs
295
- ):
296
- return tool.sort_totals(
306
+ axis: Axis = 0,
307
+ level: Level|list[Level]|None = None,
308
+ labels: list[str]|None = None,
309
+ totals_last: bool = True,
310
+ sort_remaining: bool = True,
311
+ ) -> pd.DataFrame:
312
+ """
313
+ Sort index/columns to position totals and subtotals at start or end within groups.
314
+
315
+ Convenience function that sorts common aggregate labels (totals, subtotals) to
316
+ their appropriate positions, while leaving other items in their existing order.
317
+ Uses default labels from flatbread configuration unless custom labels are provided.
318
+
319
+ Parameters
320
+ ----------
321
+ axis : Axis, default 0
322
+ Axis to sort along:
323
+ - 0 or 'index': sort the index (rows)
324
+ - 1 or 'columns': sort the columns
325
+ level : Level | list[Level] | None, default None
326
+ Index level(s) to sort. Can be level number(s), level name(s), or None for all levels.
327
+ labels : list[str] | None, default None
328
+ Custom labels to treat as totals/subtotals. If None, uses default labels from
329
+ flatbread configuration ('Totals', 'Subtotals').
330
+ totals_last : bool, default True
331
+ Whether to place totals/subtotals at the end (True) or beginning (False) of each group.
332
+ sort_remaining : bool, default True
333
+ Whether to sort non-target levels alphabetically.
334
+
335
+ Returns
336
+ -------
337
+ pd.DataFrame
338
+ DataFrame with totals/subtotals repositioned according to the specified parameters.
339
+ """
340
+ return axes.sort_totals( # type: ignore
297
341
  self._obj,
298
342
  axis = axis,
299
343
  level = level,
300
- **kwargs,
344
+ labels = labels,
345
+ totals_last = totals_last,
346
+ sort_remaining = sort_remaining,
301
347
  )
302
348
 
303
349
  def drop_totals(
304
350
  self
305
351
  ):
306
- return tot.drop_totals(self._obj)
352
+ return totals.drop_totals(self._obj)
353
+
354
+ # region io
355
+ def export_excel(
356
+ self,
357
+ filepath: str | Path,
358
+ title: str | None = None,
359
+ number_formats: dict | None = None,
360
+ border_specs: dict | None = None,
361
+ **kwargs
362
+ ) -> None:
363
+ """
364
+ Export DataFrame to Excel with automatic formatting based on flatbread configuration.
365
+
366
+ Parameters
367
+ ----------
368
+ filepath : str | Path
369
+ Path to save the Excel file
370
+ title : str, optional
371
+ Title for the worksheet
372
+ number_formats : dict, optional
373
+ Custom number formats (overrides auto-detected ones)
374
+ border_specs : dict, optional
375
+ Custom border specifications (merged with margin borders)
376
+ **kwargs
377
+ Additional arguments passed to pandasxl WorksheetManager
378
+ """
379
+ from flatbread.output.excel import export_excel
380
+ return export_excel(
381
+ self._obj,
382
+ filepath,
383
+ title=title,
384
+ number_formats=number_formats,
385
+ border_specs=border_specs,
386
+ **kwargs
387
+ )
307
388
 
308
389
  # region tooling
309
390
  def add_level(
@@ -334,7 +415,7 @@ class PitaFrame(PitaDisplayMixin):
334
415
  pd.DataFrame:
335
416
  DataFrame with the new level added to the specified axis.
336
417
  """
337
- return tool.add_level(
418
+ return axes.add_level(
338
419
  self._obj,
339
420
  value = value,
340
421
  level = level,
@@ -1,12 +1,14 @@
1
- from typing import Any, Callable
1
+ from typing import Any, Callable, Hashable, Literal, TypeAlias
2
+ from pathlib import Path
2
3
 
3
4
  import pandas as pd
4
5
 
5
- import flatbread.percentages as pct
6
- import flatbread.agg.aggregation as agg
7
- import flatbread.agg.totals as tot
8
- import flatbread.tooling as tool
9
- from flatbread.render.display import PitaDisplayMixin
6
+ import flatbread.transforms.percentages as pct
7
+ import flatbread.transforms.aggregation as agg
8
+ import flatbread.transforms.totals as totals
9
+ import flatbread.axes as axes
10
+ from flatbread.types import Axis, Level
11
+ from flatbread.output.html import PitaDisplayMixin
10
12
 
11
13
 
12
14
  @pd.api.extensions.register_series_accessor("pita")
@@ -19,7 +21,7 @@ class PitaSeries(PitaDisplayMixin):
19
21
  self,
20
22
  aggfunc: str|Callable,
21
23
  *args,
22
- label: str = None,
24
+ label: str|None = None,
23
25
  ignore_keys: str|list[str]|None = None,
24
26
  _fill: str = '',
25
27
  **kwargs,
@@ -58,10 +60,11 @@ class PitaSeries(PitaDisplayMixin):
58
60
  def add_subagg(
59
61
  self,
60
62
  aggfunc: str|Callable,
61
- level: int|str|list[int|str] = 0,
62
- label: str = None,
63
+ level: Level|list[Level] = 0,
64
+ label: str|None = None,
63
65
  include_level_name: bool = False,
64
66
  ignore_keys: str|list[str]|None = None,
67
+ skip_single_rows: bool = True,
65
68
  _fill: str = '',
66
69
  ) -> pd.Series:
67
70
  """
@@ -71,12 +74,16 @@ class PitaSeries(PitaDisplayMixin):
71
74
  ----------
72
75
  aggfunc (str|Callable):
73
76
  Function to use for aggregating the data.
74
- levels (int|str|list[int|str]):
75
- Levels to aggregate with func. Default 0.
77
+ level (int|str|list[int|str]):
78
+ Level(s) to aggregate with func. Default 0.
76
79
  label (str|None):
77
80
  Label for the aggregated rows. Default None.
81
+ include_level_name (bool):
82
+ Whether to add level name to subtotal label.
78
83
  ignore_keys (str|list[str]|None):
79
- Keys of rows to ignore when aggregating.
84
+ Keys of rows to ignore when aggregating. Default 'Totals'
85
+ skip_single_rows (bool):
86
+ Whether to skip single rows when aggregating. Default True.
80
87
  *args:
81
88
  Positional arguments to pass to func.
82
89
  **kwargs:
@@ -94,6 +101,7 @@ class PitaSeries(PitaDisplayMixin):
94
101
  label = label,
95
102
  include_level_name = include_level_name,
96
103
  ignore_keys = ignore_keys,
104
+ skip_single_rows = skip_single_rows,
97
105
  _fill = _fill,
98
106
  )
99
107
 
@@ -131,7 +139,7 @@ class PitaSeries(PitaDisplayMixin):
131
139
  Series reporting the count of each value in the original series.
132
140
  """
133
141
  s = self._obj if fillna is None else self._obj.fillna(fillna)
134
- result = s.value_counts().rename(label_n).pipe(tot.add_totals)
142
+ result = s.value_counts().rename(label_n).pipe(totals.add_totals)
135
143
  if add_pct:
136
144
  return result.pipe(
137
145
  pct.add_percentages,
@@ -145,10 +153,11 @@ class PitaSeries(PitaDisplayMixin):
145
153
  #region percentages
146
154
  def as_percentages(
147
155
  self,
148
- label_pct: str = None,
156
+ label_pct: str|None = None,
149
157
  label_totals: str|None = None,
150
- ndigits: int = None,
158
+ ndigits: int|None = None,
151
159
  base: int = 1,
160
+ apportioned_rounding: bool|None = None,
152
161
  ) -> pd.Series:
153
162
  """
154
163
  Transform data into percentages.
@@ -177,6 +186,7 @@ class PitaSeries(PitaDisplayMixin):
177
186
  label_totals = label_totals,
178
187
  ndigits = ndigits,
179
188
  base = base,
189
+ apportioned_rounding = apportioned_rounding,
180
190
  )
181
191
 
182
192
  def as_pct(self, *args, **kwargs):
@@ -189,6 +199,7 @@ class PitaSeries(PitaDisplayMixin):
189
199
  label_totals: str|None = None,
190
200
  ndigits: int|None = None,
191
201
  base: int = 1,
202
+ apportioned_rounding: bool|None = None,
192
203
  ) -> pd.DataFrame:
193
204
  """
194
205
  Add percentage column to a Series.
@@ -220,6 +231,7 @@ class PitaSeries(PitaDisplayMixin):
220
231
  label_totals = label_totals,
221
232
  ndigits = ndigits,
222
233
  base = base,
234
+ apportioned_rounding = apportioned_rounding,
223
235
  )
224
236
 
225
237
  def add_pct(self, *args, **kwargs):
@@ -247,7 +259,7 @@ class PitaSeries(PitaDisplayMixin):
247
259
  pd.Series:
248
260
  Series with totals row added.
249
261
  """
250
- return tot.add_totals(
262
+ return totals.add_totals( # type: ignore
251
263
  self._obj,
252
264
  label = label,
253
265
  ignore_keys = ignore_keys,
@@ -256,10 +268,11 @@ class PitaSeries(PitaDisplayMixin):
256
268
 
257
269
  def add_subtotals(
258
270
  self,
259
- level: int|str|list[int|str] = 0,
271
+ level: Level|list[Level] = 0,
260
272
  label: str|None = None,
261
273
  include_level_name: bool = False,
262
274
  ignore_keys: str|list[str]|None = None,
275
+ skip_single_rows: bool = True,
263
276
  _fill: str = '',
264
277
  ) -> pd.Series:
265
278
  """
@@ -267,38 +280,110 @@ class PitaSeries(PitaDisplayMixin):
267
280
 
268
281
  Parameters
269
282
  ----------
270
- levels (int|str|list[int|str]):
271
- Levels to add subtotals to. Default 0.
283
+ level (int|str|list[int|str]):
284
+ Level(s) to add subtotals to. Default 0.
272
285
  label (str|None):
273
286
  Label for the subtotals rows. Default 'Subtotals'.
287
+ include_level_name (bool):
288
+ Whether to add level name to subtotal label.
274
289
  ignore_keys (str|list[str]|None):
275
290
  Keys of rows to ignore when aggregating. Default 'Totals'
291
+ skip_single_rows (bool):
292
+ Whether to skip single rows when aggregating. Default True.
276
293
 
277
294
  Returns
278
295
  -------
279
296
  pd.Series:
280
297
  Series with subtotal rows added.
281
298
  """
282
- return tot.add_subtotals(
299
+ return totals.add_subtotals( # type: ignore
283
300
  self._obj,
284
301
  level = level,
285
302
  label = label,
286
303
  include_level_name = include_level_name,
287
304
  ignore_keys = ignore_keys,
305
+ skip_single_rows = skip_single_rows,
288
306
  _fill = _fill,
289
307
  )
290
308
 
291
309
  def sort_totals(
292
310
  self,
293
- axis: int = 0,
294
- level: int = 0,
295
- **kwargs
296
- ):
297
- return tool.sort_totals(
311
+ axis: Axis = 0,
312
+ level: Level|list[Level]|None = None,
313
+ labels: list[str]|None = None,
314
+ totals_last: bool = True,
315
+ sort_remaining: bool = True,
316
+ ) -> pd.Series:
317
+ """
318
+ Sort index/columns to position totals and subtotals at start or end within groups.
319
+
320
+ Convenience function that sorts common aggregate labels (totals, subtotals) to
321
+ their appropriate positions, while leaving other items in their existing order.
322
+ Uses default labels from flatbread configuration unless custom labels are provided.
323
+
324
+ Parameters
325
+ ----------
326
+ axis : Axis, default 0
327
+ Axis to sort along:
328
+ - 0 or 'index': sort the index (rows)
329
+ - 1 or 'columns': sort the columns
330
+ level : Level | list[Level] | None, default None
331
+ Index level(s) to sort. Can be level number(s), level name(s), or None for all levels.
332
+ labels : list[str] | None, default None
333
+ Custom labels to treat as totals/subtotals. If None, uses default labels from
334
+ flatbread configuration ('Totals', 'Subtotals').
335
+ totals_last : bool, default True
336
+ Whether to place totals/subtotals at the end (True) or beginning (False) of each group.
337
+ sort_remaining : bool, default True
338
+ Whether to sort non-target levels alphabetically.
339
+
340
+ Returns
341
+ -------
342
+ pd.Series
343
+ Series with totals/subtotals repositioned according to the specified parameters.
344
+ """
345
+ return axes.sort_totals( # type: ignore
298
346
  self._obj,
299
347
  axis = axis,
300
348
  level = level,
301
- **kwargs,
349
+ labels = labels,
350
+ totals_last = totals_last,
351
+ sort_remaining = sort_remaining,
352
+ )
353
+
354
+ # region io
355
+ def export_excel(
356
+ self,
357
+ filepath: str | Path,
358
+ title: str | None = None,
359
+ number_formats: dict | None = None,
360
+ border_specs: dict | None = None,
361
+ **kwargs
362
+ ) -> None:
363
+ """
364
+ Export Series to Excel with automatic formatting based on flatbread configuration.
365
+
366
+ Parameters
367
+ ----------
368
+ filepath : str | Path
369
+ Path to save the Excel file
370
+ title : str, optional
371
+ Title for the worksheet
372
+ number_formats : dict, optional
373
+ Custom number formats (overrides auto-detected ones)
374
+ border_specs : dict, optional
375
+ Custom border specifications (merged with margin borders)
376
+ **kwargs
377
+ Additional arguments passed to pandasxl WorksheetManager
378
+ """
379
+ from flatbread.output.excel import export_excel
380
+ return export_excel(
381
+ self._obj,
382
+ filepath,
383
+ title=title,
384
+ number_formats=number_formats,
385
+ border_specs=border_specs,
386
+ **kwargs
302
387
  )
303
388
 
304
389
  # region tooling
@@ -307,7 +392,7 @@ class PitaSeries(PitaDisplayMixin):
307
392
  value: Any,
308
393
  level: int = 0,
309
394
  level_name: Any = None,
310
- axis: int = 0,
395
+ axis: Axis = 0,
311
396
  ):
312
397
  """
313
398
  Add a level containing the specified value to a Series index.
@@ -322,7 +407,7 @@ class PitaSeries(PitaDisplayMixin):
322
407
  Position to insert the new level. Defaults to 0 (start).
323
408
  level_name (Any, optional):
324
409
  Name for the new level. Defaults to None.
325
- axis (Axis):
410
+ axis (int | Literal["index", "columns", "both"]):
326
411
  Added for symmetry with DataFrame method.
327
412
 
328
413
  Returns
@@ -330,7 +415,7 @@ class PitaSeries(PitaDisplayMixin):
330
415
  pd.Series:
331
416
  Series with the new level added to the specified axis.
332
417
  """
333
- return tool.add_level(
418
+ return axes.add_level(
334
419
  self._obj,
335
420
  value = value,
336
421
  level = level,