flatbread 0.1.1__tar.gz → 0.1.3__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.1 → flatbread-0.1.3}/PKG-INFO +1 -1
  2. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/config/config.defaults.json +10 -1
  3. flatbread-0.1.3/flatbread/config.py +133 -0
  4. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/constants.py +16 -3
  5. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/display.py +46 -0
  6. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/tablespec.py +43 -5
  7. {flatbread-0.1.1 → flatbread-0.1.3}/pyproject.toml +1 -1
  8. flatbread-0.1.1/flatbread/config.py +0 -43
  9. {flatbread-0.1.1 → flatbread-0.1.3}/.gitignore +0 -0
  10. {flatbread-0.1.1 → flatbread-0.1.3}/environment.yml +0 -0
  11. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/__init__.py +0 -0
  12. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/accessors/dataframe.py +0 -0
  13. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/accessors/index.py +0 -0
  14. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/accessors/series.py +0 -0
  15. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/agg/aggregation.py +0 -0
  16. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/agg/totals.py +0 -0
  17. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/chaining.py +0 -0
  18. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/percentages.py +0 -0
  19. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/config.py +0 -0
  20. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/template.jinja.html +0 -0
  21. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/template.py +0 -0
  22. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/testing/dataframe.py +0 -0
  23. {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/tooling.py +0 -0
  24. {flatbread-0.1.1 → flatbread-0.1.3}/license.md +0 -0
  25. {flatbread-0.1.1 → flatbread-0.1.3}/readme.md +0 -0
  26. {flatbread-0.1.1 → flatbread-0.1.3}/tests/__init__.py +0 -0
  27. {flatbread-0.1.1 → flatbread-0.1.3}/tests/aggregate/__init__.py +0 -0
  28. {flatbread-0.1.1 → flatbread-0.1.3}/tests/aggregate/test_percentages.py +0 -0
  29. {flatbread-0.1.1 → flatbread-0.1.3}/tests/aggregate/test_totals.py +0 -0
  30. {flatbread-0.1.1 → flatbread-0.1.3}/tests/test_axes.py +0 -0
  31. {flatbread-0.1.1 → flatbread-0.1.3}/tests/test_levels.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatbread
3
- Version: 0.1.1
3
+ Version: 0.1.3
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>
@@ -13,5 +13,14 @@
13
13
  "ndigits": -1,
14
14
  "base": 1
15
15
  },
16
- "locale": null
16
+ "locale": null,
17
+ "format_presets": {
18
+ "currency_eur": {
19
+ "dtypes": ["float", "int"],
20
+ "options": {
21
+ "style": "currency",
22
+ "currency": "EUR"
23
+ }
24
+ }
25
+ }
17
26
  }
@@ -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
@@ -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.1"
7
+ version = "0.1.3"
8
8
  description = "Pandas extension for aggregation and tabular display"
9
9
  readme = "readme.md"
10
10
  requires-python = ">=3.10"
@@ -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