additory 0.1.0a1__py3-none-any.whl
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.
- additory/__init__.py +15 -0
- additory/analysis/__init__.py +48 -0
- additory/analysis/cardinality.py +126 -0
- additory/analysis/correlations.py +124 -0
- additory/analysis/distributions.py +376 -0
- additory/analysis/quality.py +158 -0
- additory/analysis/scan.py +400 -0
- additory/augment/__init__.py +24 -0
- additory/augment/augmentor.py +653 -0
- additory/augment/builtin_lists.py +430 -0
- additory/augment/distributions.py +22 -0
- additory/augment/forecast.py +1132 -0
- additory/augment/list_registry.py +177 -0
- additory/augment/smote.py +320 -0
- additory/augment/strategies.py +883 -0
- additory/common/__init__.py +157 -0
- additory/common/backend.py +355 -0
- additory/common/column_utils.py +191 -0
- additory/common/distributions.py +737 -0
- additory/common/exceptions.py +62 -0
- additory/common/lists.py +229 -0
- additory/common/patterns.py +240 -0
- additory/common/resolver.py +567 -0
- additory/common/sample_data.py +182 -0
- additory/common/validation.py +197 -0
- additory/core/__init__.py +27 -0
- additory/core/ast_builder.py +165 -0
- additory/core/backends/__init__.py +23 -0
- additory/core/backends/arrow_bridge.py +476 -0
- additory/core/backends/cudf_bridge.py +355 -0
- additory/core/column_positioning.py +358 -0
- additory/core/compiler_polars.py +166 -0
- additory/core/config.py +342 -0
- additory/core/enhanced_cache_manager.py +1119 -0
- additory/core/enhanced_matchers.py +473 -0
- additory/core/enhanced_version_manager.py +325 -0
- additory/core/executor.py +59 -0
- additory/core/integrity_manager.py +477 -0
- additory/core/loader.py +190 -0
- additory/core/logging.py +24 -0
- additory/core/memory_manager.py +547 -0
- additory/core/namespace_manager.py +657 -0
- additory/core/parser.py +176 -0
- additory/core/polars_expression_engine.py +551 -0
- additory/core/registry.py +176 -0
- additory/core/sample_data_manager.py +492 -0
- additory/core/user_namespace.py +751 -0
- additory/core/validator.py +27 -0
- additory/dynamic_api.py +308 -0
- additory/expressions/__init__.py +26 -0
- additory/expressions/engine.py +551 -0
- additory/expressions/parser.py +176 -0
- additory/expressions/proxy.py +546 -0
- additory/expressions/registry.py +313 -0
- additory/expressions/samples.py +492 -0
- additory/synthetic/__init__.py +101 -0
- additory/synthetic/api.py +220 -0
- additory/synthetic/common_integration.py +314 -0
- additory/synthetic/config.py +262 -0
- additory/synthetic/engines.py +529 -0
- additory/synthetic/exceptions.py +180 -0
- additory/synthetic/file_managers.py +518 -0
- additory/synthetic/generator.py +702 -0
- additory/synthetic/generator_parser.py +68 -0
- additory/synthetic/integration.py +319 -0
- additory/synthetic/models.py +241 -0
- additory/synthetic/pattern_resolver.py +573 -0
- additory/synthetic/performance.py +469 -0
- additory/synthetic/polars_integration.py +464 -0
- additory/synthetic/proxy.py +60 -0
- additory/synthetic/schema_parser.py +685 -0
- additory/synthetic/validator.py +553 -0
- additory/utilities/__init__.py +53 -0
- additory/utilities/encoding.py +600 -0
- additory/utilities/games.py +300 -0
- additory/utilities/keys.py +8 -0
- additory/utilities/lookup.py +103 -0
- additory/utilities/matchers.py +216 -0
- additory/utilities/resolvers.py +286 -0
- additory/utilities/settings.py +167 -0
- additory/utilities/units.py +746 -0
- additory/utilities/validators.py +153 -0
- additory-0.1.0a1.dist-info/METADATA +293 -0
- additory-0.1.0a1.dist-info/RECORD +87 -0
- additory-0.1.0a1.dist-info/WHEEL +5 -0
- additory-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- additory-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized Sample Dataset Management
|
|
3
|
+
|
|
4
|
+
Provides sample datasets for demonstrations across all additory modules.
|
|
5
|
+
Sample datasets are stored as .add files in reference/ directories and
|
|
6
|
+
loaded on-demand using the existing .add file parser.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from additory.common.sample_data import get_sample_dataset
|
|
10
|
+
|
|
11
|
+
# For augment
|
|
12
|
+
df = get_sample_dataset("augment", "sample")
|
|
13
|
+
|
|
14
|
+
# For expressions (future)
|
|
15
|
+
df = get_sample_dataset("expressions", "sample")
|
|
16
|
+
df_unclean = get_sample_dataset("expressions", "sample_unclean")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import polars as pl
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
from additory.common.exceptions import ValidationError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_sample_dataset(
|
|
28
|
+
module: str = "augment",
|
|
29
|
+
block: str = "sample",
|
|
30
|
+
dataset_type: str = "clean"
|
|
31
|
+
) -> pl.DataFrame:
|
|
32
|
+
"""
|
|
33
|
+
Load a sample dataset from .add files.
|
|
34
|
+
|
|
35
|
+
This function provides centralized access to sample datasets across
|
|
36
|
+
all additory modules (augment, expressions, utilities). Sample datasets
|
|
37
|
+
are stored as .add files in the reference/ directory structure.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
module: Module name ("augment", "expressions", "utilities")
|
|
41
|
+
block: Block name within the .add file ("sample" for augment)
|
|
42
|
+
dataset_type: Type of sample data ("clean" or "unclean")
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Polars DataFrame with sample data
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValidationError: If module, block, or dataset_type not found
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> # Load augment sample dataset
|
|
52
|
+
>>> df = get_sample_dataset("augment", "sample")
|
|
53
|
+
>>> print(df.shape)
|
|
54
|
+
(50, 10)
|
|
55
|
+
|
|
56
|
+
>>> # Load expressions sample dataset (future)
|
|
57
|
+
>>> df = get_sample_dataset("expressions", "sample", "clean")
|
|
58
|
+
>>> df_unclean = get_sample_dataset("expressions", "sample", "unclean")
|
|
59
|
+
|
|
60
|
+
Sample Dataset Structure (augment):
|
|
61
|
+
- id: Sequential numeric IDs (1-50)
|
|
62
|
+
- emp_id: Employee IDs with pattern (EMP_001 - EMP_050)
|
|
63
|
+
- order_id: Order IDs with different padding (ORD_0001 - ORD_0050)
|
|
64
|
+
- age: Age values (18-65 range)
|
|
65
|
+
- salary: Salary values (40k-120k range)
|
|
66
|
+
- first_name: First names from builtin list
|
|
67
|
+
- last_name: Last names from builtin list
|
|
68
|
+
- department: Departments from builtin list
|
|
69
|
+
- status: Status values from builtin list
|
|
70
|
+
- region: Geographic regions (North, South, East, West)
|
|
71
|
+
"""
|
|
72
|
+
# Construct path to .add file
|
|
73
|
+
base_path = Path(__file__).parent.parent.parent / "reference"
|
|
74
|
+
|
|
75
|
+
if module == "augment":
|
|
76
|
+
add_file_path = base_path / "augment_definitions" / f"{block}_0.1.add"
|
|
77
|
+
elif module == "expressions":
|
|
78
|
+
add_file_path = base_path / "expressions_definitions" / f"{block}_0.1.add"
|
|
79
|
+
elif module == "utilities":
|
|
80
|
+
add_file_path = base_path / "utilities_definitions" / f"{block}_0.1.add"
|
|
81
|
+
else:
|
|
82
|
+
raise ValidationError(
|
|
83
|
+
f"Unknown module '{module}'. "
|
|
84
|
+
f"Valid modules: augment, expressions, utilities"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Check if file exists
|
|
88
|
+
if not add_file_path.exists():
|
|
89
|
+
raise ValidationError(
|
|
90
|
+
f"Sample dataset file not found: {add_file_path}\n"
|
|
91
|
+
f"Module: {module}, Block: {block}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Load and parse .add file
|
|
95
|
+
try:
|
|
96
|
+
with open(add_file_path, 'r') as f:
|
|
97
|
+
content = yaml.safe_load(f)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise ValidationError(
|
|
100
|
+
f"Failed to parse sample dataset file: {add_file_path}\n"
|
|
101
|
+
f"Error: {e}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Extract sample data
|
|
105
|
+
sample_section = content.get("sample", {})
|
|
106
|
+
|
|
107
|
+
if not sample_section:
|
|
108
|
+
raise ValidationError(
|
|
109
|
+
f"No 'sample' section found in {add_file_path}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Get the requested dataset type (clean or unclean)
|
|
113
|
+
dataset = sample_section.get(dataset_type)
|
|
114
|
+
|
|
115
|
+
if dataset is None:
|
|
116
|
+
available_types = list(sample_section.keys())
|
|
117
|
+
raise ValidationError(
|
|
118
|
+
f"Dataset type '{dataset_type}' not found in {add_file_path}\n"
|
|
119
|
+
f"Available types: {available_types}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Convert to Polars DataFrame
|
|
123
|
+
try:
|
|
124
|
+
df = pl.DataFrame(dataset)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
raise ValidationError(
|
|
127
|
+
f"Failed to create DataFrame from sample data: {e}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return df
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def list_available_samples() -> dict:
|
|
134
|
+
"""
|
|
135
|
+
List all available sample datasets.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dictionary mapping module names to available samples
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> samples = list_available_samples()
|
|
142
|
+
>>> print(samples)
|
|
143
|
+
{
|
|
144
|
+
'augment': ['sample'],
|
|
145
|
+
'expressions': ['sample'],
|
|
146
|
+
'utilities': []
|
|
147
|
+
}
|
|
148
|
+
"""
|
|
149
|
+
base_path = Path(__file__).parent.parent.parent / "reference"
|
|
150
|
+
available = {}
|
|
151
|
+
|
|
152
|
+
# Check augment
|
|
153
|
+
augment_path = base_path / "augment_definitions"
|
|
154
|
+
if augment_path.exists():
|
|
155
|
+
available['augment'] = [
|
|
156
|
+
f.stem.rsplit('_', 1)[0] # Remove version suffix
|
|
157
|
+
for f in augment_path.glob("*.add")
|
|
158
|
+
]
|
|
159
|
+
else:
|
|
160
|
+
available['augment'] = []
|
|
161
|
+
|
|
162
|
+
# Check expressions
|
|
163
|
+
expressions_path = base_path / "expressions_definitions"
|
|
164
|
+
if expressions_path.exists():
|
|
165
|
+
available['expressions'] = [
|
|
166
|
+
f.stem.rsplit('_', 1)[0] # Remove version suffix
|
|
167
|
+
for f in expressions_path.glob("*.add")
|
|
168
|
+
]
|
|
169
|
+
else:
|
|
170
|
+
available['expressions'] = []
|
|
171
|
+
|
|
172
|
+
# Check utilities
|
|
173
|
+
utilities_path = base_path / "utilities_definitions"
|
|
174
|
+
if utilities_path.exists():
|
|
175
|
+
available['utilities'] = [
|
|
176
|
+
f.stem.rsplit('_', 1)[0] # Remove version suffix
|
|
177
|
+
for f in utilities_path.glob("*.add")
|
|
178
|
+
]
|
|
179
|
+
else:
|
|
180
|
+
available['utilities'] = []
|
|
181
|
+
|
|
182
|
+
return available
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common Validation Utilities
|
|
3
|
+
|
|
4
|
+
Provides consistent validation across all additory modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, List, Union
|
|
8
|
+
from .backend import is_dataframe, detect_backend
|
|
9
|
+
from .exceptions import ValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_dataframe(df: Any, name: str = "dataframe") -> None:
|
|
13
|
+
"""
|
|
14
|
+
Validate that input is a supported dataframe type.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
df: Input to validate
|
|
18
|
+
name: Name for error messages
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValidationError: If not a supported dataframe or empty
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
>>> validate_dataframe(df, "input dataframe")
|
|
25
|
+
"""
|
|
26
|
+
if not is_dataframe(df):
|
|
27
|
+
raise ValidationError(
|
|
28
|
+
f"{name} must be a DataFrame (pandas, polars, or cudf). "
|
|
29
|
+
f"Got: {type(df)}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if len(df) == 0:
|
|
33
|
+
raise ValidationError(f"{name} is empty")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_columns_exist(df: Any, columns: Union[str, List[str]],
|
|
37
|
+
df_name: str = "dataframe") -> None:
|
|
38
|
+
"""
|
|
39
|
+
Validate that columns exist in dataframe.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
df: Dataframe to check
|
|
43
|
+
columns: Column name(s) to validate
|
|
44
|
+
df_name: Name for error messages
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValidationError: If columns don't exist
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
>>> validate_columns_exist(df, ['col1', 'col2'], "my_dataframe")
|
|
51
|
+
>>> validate_columns_exist(df, 'single_col')
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(columns, str):
|
|
54
|
+
columns = [columns]
|
|
55
|
+
|
|
56
|
+
df_columns = list(df.columns)
|
|
57
|
+
missing_columns = [col for col in columns if col not in df_columns]
|
|
58
|
+
|
|
59
|
+
if missing_columns:
|
|
60
|
+
raise ValidationError(
|
|
61
|
+
f"Column(s) {missing_columns} not found in {df_name}. "
|
|
62
|
+
f"Available columns: {df_columns}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def validate_positive_number(value: Union[int, float], param_name: str) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Validate that value is a positive number.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
value: Value to validate
|
|
72
|
+
param_name: Parameter name for error messages
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValidationError: If not a positive number
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
>>> validate_positive_number(10, "max_categories")
|
|
79
|
+
"""
|
|
80
|
+
if not isinstance(value, (int, float)):
|
|
81
|
+
raise ValidationError(f"{param_name} must be a number, got {type(value)}")
|
|
82
|
+
|
|
83
|
+
if value <= 0:
|
|
84
|
+
raise ValidationError(f"{param_name} must be positive, got {value}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_non_negative_number(value: Union[int, float], param_name: str) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Validate that value is a non-negative number.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
value: Value to validate
|
|
93
|
+
param_name: Parameter name for error messages
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValidationError: If not a non-negative number
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
>>> validate_non_negative_number(0, "min_value")
|
|
100
|
+
"""
|
|
101
|
+
if not isinstance(value, (int, float)):
|
|
102
|
+
raise ValidationError(f"{param_name} must be a number, got {type(value)}")
|
|
103
|
+
|
|
104
|
+
if value < 0:
|
|
105
|
+
raise ValidationError(f"{param_name} must be non-negative, got {value}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def validate_parameter_choice(value: Any, choices: List[Any],
|
|
109
|
+
param_name: str) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Validate that parameter value is in allowed choices.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
value: Value to validate
|
|
115
|
+
choices: List of allowed values
|
|
116
|
+
param_name: Parameter name for error messages
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValidationError: If value not in choices
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
>>> validate_parameter_choice('after', ['before', 'after', 'end'], 'position')
|
|
123
|
+
"""
|
|
124
|
+
if value not in choices:
|
|
125
|
+
raise ValidationError(
|
|
126
|
+
f"Invalid {param_name}: '{value}'. "
|
|
127
|
+
f"Must be one of: {choices}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate_ratio(value: float, param_name: str) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Validate that value is a ratio between 0 and 1.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
value: Value to validate
|
|
137
|
+
param_name: Parameter name for error messages
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValidationError: If not a valid ratio
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
>>> validate_ratio(0.5, "max_cardinality_ratio")
|
|
144
|
+
"""
|
|
145
|
+
if not isinstance(value, (int, float)):
|
|
146
|
+
raise ValidationError(f"{param_name} must be a number, got {type(value)}")
|
|
147
|
+
|
|
148
|
+
if not 0.0 <= value <= 1.0:
|
|
149
|
+
raise ValidationError(f"{param_name} must be between 0.0 and 1.0, got {value}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def validate_string_not_empty(value: str, param_name: str) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Validate that string is not empty.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
value: String to validate
|
|
158
|
+
param_name: Parameter name for error messages
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValidationError: If string is empty or not a string
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
>>> validate_string_not_empty("column_name", "column")
|
|
165
|
+
"""
|
|
166
|
+
if not isinstance(value, str):
|
|
167
|
+
raise ValidationError(f"{param_name} must be a string, got {type(value)}")
|
|
168
|
+
|
|
169
|
+
if not value.strip():
|
|
170
|
+
raise ValidationError(f"{param_name} cannot be empty")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def validate_integer_in_range(value: int, param_name: str,
|
|
174
|
+
min_val: int = None, max_val: int = None) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Validate that integer is within specified range.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
value: Integer to validate
|
|
180
|
+
param_name: Parameter name for error messages
|
|
181
|
+
min_val: Minimum allowed value (inclusive)
|
|
182
|
+
max_val: Maximum allowed value (inclusive)
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ValidationError: If not an integer or out of range
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
>>> validate_integer_in_range(50, "max_categories", min_val=1, max_val=200)
|
|
189
|
+
"""
|
|
190
|
+
if not isinstance(value, int):
|
|
191
|
+
raise ValidationError(f"{param_name} must be an integer, got {type(value)}")
|
|
192
|
+
|
|
193
|
+
if min_val is not None and value < min_val:
|
|
194
|
+
raise ValidationError(f"{param_name} must be >= {min_val}, got {value}")
|
|
195
|
+
|
|
196
|
+
if max_val is not None and value > max_val:
|
|
197
|
+
raise ValidationError(f"{param_name} must be <= {max_val}, got {value}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# core/__init__.py
|
|
2
|
+
# Expose core engine components
|
|
3
|
+
|
|
4
|
+
from .executor import execute_expression
|
|
5
|
+
from .registry import (
|
|
6
|
+
resolve_formula,
|
|
7
|
+
set_formula_version,
|
|
8
|
+
set_formula_root,
|
|
9
|
+
set_custom_formula_path,
|
|
10
|
+
)
|
|
11
|
+
from .loader import load_expression
|
|
12
|
+
from .parser import parse_expression
|
|
13
|
+
from .validator import validate_expression
|
|
14
|
+
from .logging import log_info, log_warning
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"execute_expression",
|
|
18
|
+
"resolve_formula",
|
|
19
|
+
"set_formula_version",
|
|
20
|
+
"set_formula_root",
|
|
21
|
+
"set_custom_formula_path",
|
|
22
|
+
"load_expression",
|
|
23
|
+
"parse_expression",
|
|
24
|
+
"validate_expression",
|
|
25
|
+
"log_info",
|
|
26
|
+
"log_warning",
|
|
27
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# ast_builder.py
|
|
2
|
+
#
|
|
3
|
+
# Extended AST builder for additory DSL.
|
|
4
|
+
# Backward compatible with minimal arithmetic DSL.
|
|
5
|
+
# Adds:
|
|
6
|
+
# - comparisons
|
|
7
|
+
# - boolean logic
|
|
8
|
+
# - ternary (Python-style: a if cond else b)
|
|
9
|
+
# - function calls (min, max, abs, log, exp)
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_ast_from_expression(expr: str) -> dict:
|
|
16
|
+
"""
|
|
17
|
+
Convert a Python-like expression string into our internal AST format.
|
|
18
|
+
Uses Python's ast module as a parser, then transforms nodes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if not expr or not expr.strip():
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
py_ast = ast.parse(expr, mode="eval")
|
|
25
|
+
return _convert(py_ast.body)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _convert(node):
|
|
29
|
+
"""Convert Python AST → additory AST."""
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------
|
|
32
|
+
# Literals
|
|
33
|
+
# ------------------------------------------------------------
|
|
34
|
+
if isinstance(node, ast.Constant):
|
|
35
|
+
return {"type": "literal", "value": node.value}
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------
|
|
38
|
+
# Column reference
|
|
39
|
+
# ------------------------------------------------------------
|
|
40
|
+
if isinstance(node, ast.Name):
|
|
41
|
+
return {"type": "column", "name": node.id}
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------
|
|
44
|
+
# Binary arithmetic: + - * / **
|
|
45
|
+
# ------------------------------------------------------------
|
|
46
|
+
if isinstance(node, ast.BinOp):
|
|
47
|
+
return {
|
|
48
|
+
"type": "binary",
|
|
49
|
+
"op": _op_symbol(node.op),
|
|
50
|
+
"left": _convert(node.left),
|
|
51
|
+
"right": _convert(node.right),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------
|
|
55
|
+
# Unary arithmetic: -x, +x
|
|
56
|
+
# ------------------------------------------------------------
|
|
57
|
+
if isinstance(node, ast.UnaryOp):
|
|
58
|
+
if isinstance(node.op, ast.UAdd):
|
|
59
|
+
return _convert(node.operand)
|
|
60
|
+
if isinstance(node.op, ast.USub):
|
|
61
|
+
return {
|
|
62
|
+
"type": "binary",
|
|
63
|
+
"op": "*",
|
|
64
|
+
"left": {"type": "literal", "value": -1},
|
|
65
|
+
"right": _convert(node.operand),
|
|
66
|
+
}
|
|
67
|
+
if isinstance(node.op, ast.Not):
|
|
68
|
+
return {
|
|
69
|
+
"type": "unary_bool",
|
|
70
|
+
"op": "not",
|
|
71
|
+
"value": _convert(node.operand),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------
|
|
75
|
+
# Boolean operations: and/or
|
|
76
|
+
# ------------------------------------------------------------
|
|
77
|
+
if isinstance(node, ast.BoolOp):
|
|
78
|
+
op = "and" if isinstance(node.op, ast.And) else "or"
|
|
79
|
+
return {
|
|
80
|
+
"type": "bool_op",
|
|
81
|
+
"op": op,
|
|
82
|
+
"values": [_convert(v) for v in node.values],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------
|
|
86
|
+
# Comparisons: == != > < >= <=
|
|
87
|
+
# ------------------------------------------------------------
|
|
88
|
+
if isinstance(node, ast.Compare):
|
|
89
|
+
# Python allows chained comparisons: a < b < c
|
|
90
|
+
# We only support simple binary comparisons
|
|
91
|
+
if len(node.ops) != 1 or len(node.comparators) != 1:
|
|
92
|
+
raise NotImplementedError("Chained comparisons not supported")
|
|
93
|
+
|
|
94
|
+
op = _cmp_symbol(node.ops[0])
|
|
95
|
+
return {
|
|
96
|
+
"type": "cmp",
|
|
97
|
+
"op": op,
|
|
98
|
+
"left": _convert(node.left),
|
|
99
|
+
"right": _convert(node.comparators[0]),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------
|
|
103
|
+
# Ternary: a if cond else b
|
|
104
|
+
# ------------------------------------------------------------
|
|
105
|
+
if isinstance(node, ast.IfExp):
|
|
106
|
+
return {
|
|
107
|
+
"type": "if_expr",
|
|
108
|
+
"cond": _convert(node.test),
|
|
109
|
+
"then": _convert(node.body),
|
|
110
|
+
"else": _convert(node.orelse),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------
|
|
114
|
+
# Function calls: min, max, abs, log, exp
|
|
115
|
+
# ------------------------------------------------------------
|
|
116
|
+
if isinstance(node, ast.Call):
|
|
117
|
+
if not isinstance(node.func, ast.Name):
|
|
118
|
+
raise NotImplementedError("Only simple function calls supported")
|
|
119
|
+
|
|
120
|
+
name = node.func.id
|
|
121
|
+
args = [_convert(a) for a in node.args]
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"type": "call",
|
|
125
|
+
"name": name,
|
|
126
|
+
"args": args,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
raise NotImplementedError(f"Unsupported AST node: {type(node)}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _op_symbol(op):
|
|
133
|
+
"""Map Python AST operator → string symbol."""
|
|
134
|
+
if isinstance(op, ast.Add):
|
|
135
|
+
return "+"
|
|
136
|
+
if isinstance(op, ast.Sub):
|
|
137
|
+
return "-"
|
|
138
|
+
if isinstance(op, ast.Mult):
|
|
139
|
+
return "*"
|
|
140
|
+
if isinstance(op, ast.Div):
|
|
141
|
+
return "/"
|
|
142
|
+
if isinstance(op, ast.Pow):
|
|
143
|
+
return "**"
|
|
144
|
+
if isinstance(op, ast.Mod):
|
|
145
|
+
return "%"
|
|
146
|
+
if isinstance(op, ast.FloorDiv):
|
|
147
|
+
return "//"
|
|
148
|
+
raise NotImplementedError(f"Unsupported operator: {type(op)}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _cmp_symbol(op):
|
|
152
|
+
"""Map Python AST comparison operator → string symbol."""
|
|
153
|
+
if isinstance(op, ast.Eq):
|
|
154
|
+
return "=="
|
|
155
|
+
if isinstance(op, ast.NotEq):
|
|
156
|
+
return "!="
|
|
157
|
+
if isinstance(op, ast.Gt):
|
|
158
|
+
return ">"
|
|
159
|
+
if isinstance(op, ast.Lt):
|
|
160
|
+
return "<"
|
|
161
|
+
if isinstance(op, ast.GtE):
|
|
162
|
+
return ">="
|
|
163
|
+
if isinstance(op, ast.LtE):
|
|
164
|
+
return "<="
|
|
165
|
+
raise NotImplementedError(f"Unsupported comparison operator: {type(op)}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# additory/core/backends/__init__.py
|
|
2
|
+
# Backend support system
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Backend Support Module
|
|
6
|
+
|
|
7
|
+
This module provides universal backend support for dataframes:
|
|
8
|
+
- Arrow bridge for cross-backend compatibility
|
|
9
|
+
- Enhanced cuDF support with GPU acceleration
|
|
10
|
+
- Memory management and cleanup
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Backend functionality
|
|
14
|
+
from .arrow_bridge import EnhancedArrowBridge, ArrowBridgeError
|
|
15
|
+
from .cudf_bridge import get_cudf_bridge, EnhancedCuDFBridge, CuDFBridgeError
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'EnhancedArrowBridge',
|
|
19
|
+
'ArrowBridgeError',
|
|
20
|
+
'get_cudf_bridge',
|
|
21
|
+
'EnhancedCuDFBridge',
|
|
22
|
+
'CuDFBridgeError'
|
|
23
|
+
]
|