flatbread 0.1.1__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.
- {flatbread-0.1.1 → flatbread-0.1.2}/PKG-INFO +1 -1
- flatbread-0.1.2/flatbread/config.py +133 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/pyproject.toml +1 -1
- flatbread-0.1.1/flatbread/config.py +0 -43
- {flatbread-0.1.1 → flatbread-0.1.2}/.gitignore +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/environment.yml +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/__init__.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/accessors/dataframe.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/accessors/index.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/accessors/series.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/agg/aggregation.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/agg/totals.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/chaining.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/config/config.defaults.json +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/percentages.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/render/config.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/render/constants.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/render/display.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/render/tablespec.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/render/template.jinja.html +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/render/template.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/testing/dataframe.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/flatbread/tooling.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/license.md +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/readme.md +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/tests/__init__.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/tests/aggregate/__init__.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/tests/aggregate/test_percentages.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/tests/aggregate/test_totals.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/tests/test_axes.py +0 -0
- {flatbread-0.1.1 → flatbread-0.1.2}/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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|