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.
- {flatbread-0.1.1 → flatbread-0.1.3}/PKG-INFO +1 -1
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/config/config.defaults.json +10 -1
- flatbread-0.1.3/flatbread/config.py +133 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/constants.py +16 -3
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/display.py +46 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/tablespec.py +43 -5
- {flatbread-0.1.1 → flatbread-0.1.3}/pyproject.toml +1 -1
- flatbread-0.1.1/flatbread/config.py +0 -43
- {flatbread-0.1.1 → flatbread-0.1.3}/.gitignore +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/environment.yml +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/__init__.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/accessors/dataframe.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/accessors/index.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/accessors/series.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/agg/aggregation.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/agg/totals.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/chaining.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/percentages.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/config.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/template.jinja.html +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/render/template.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/testing/dataframe.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/flatbread/tooling.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/license.md +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/readme.md +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/tests/__init__.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/tests/aggregate/__init__.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/tests/aggregate/test_percentages.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/tests/aggregate/test_totals.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/tests/test_axes.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.3}/tests/test_levels.py +0 -0
|
@@ -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:
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
|
@@ -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
|