satisfactoscript 1.0.0b4__tar.gz → 1.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.
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/PKG-INFO +6 -3
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/README.md +1 -1
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/pyproject.toml +8 -2
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/__init__.py +2 -1
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/snowpark.py +6 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/sql_base.py +57 -17
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/cli.py +11 -27
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/interpreter.py +6 -2
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/ir.py +31 -1
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/op_catalog.py +13 -2
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/operations.py +108 -45
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/rule_executor.py +22 -5
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/llm_provider.py +105 -4
- satisfactoscript-1.2.0/src/satisfactoscript/serving/__init__.py +20 -0
- satisfactoscript-1.2.0/src/satisfactoscript/serving/_response_serializer.py +96 -0
- satisfactoscript-1.2.0/src/satisfactoscript/serving/chat_model.py +122 -0
- satisfactoscript-1.2.0/src/satisfactoscript/utils.py +130 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/PKG-INFO +6 -3
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/SOURCES.txt +6 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/requires.txt +4 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_snowpark.py +10 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_sql_base.py +65 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core.py +60 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core_join.py +90 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_ir.py +48 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_llm_provider.py +107 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_op_catalog.py +9 -1
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_registry_import_paths.py +8 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_rule_executor.py +31 -0
- satisfactoscript-1.2.0/tests/test_serving_chat_model.py +147 -0
- satisfactoscript-1.2.0/tests/test_serving_response_serializer.py +183 -0
- satisfactoscript-1.2.0/tests/test_utils_logging.py +95 -0
- satisfactoscript-1.0.0b4/src/satisfactoscript/utils.py +0 -66
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/setup.cfg +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/__init__.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/builder_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/dictionary_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/exporter.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/history.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/hub.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/lineage_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/models.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/orchestrator.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/quality_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/resolver.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/user_profile.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/__init__.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/bigquery.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/spark.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/__init__.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/backend.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/catalog_inspector.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/config.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/context.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/core.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/environment.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/json_schema.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/loaders.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/patterns.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/registry.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/rule_analyzer.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/rule_planner.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/sandbox.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/schema_loader.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/writer.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/lineage/__init__.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/lineage/dictionary.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/lineage/renderer.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/lineage/tracker.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/__init__.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/alerts.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/checks.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/contracts.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/history.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/monitor.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/reporter.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/registry.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/__init__.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/builder.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/extractor.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/glossary.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/semantic.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/validator.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/sinks/__init__.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/sinks/jdbc.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/spark_factory.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/dependency_links.txt +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/entry_points.txt +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/top_level.txt +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_bigquery.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_protocol.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_spark.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_builder_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_catalog_inspector.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_cli.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_config.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core_connect_patch.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core_env_detection.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core_username.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_dictionary_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_dummy.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_engine_fake_backend.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_engine_with_backend.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_history.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_hub.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_interpreter.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_json_schema.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_lineage_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_lineage_dictionary.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_lineage_renderer.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_lineage_tracker.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_loaders.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_observability.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_orchestrator.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_patterns.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_quality_agent.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_registry.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_resolver.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_rule_analyzer.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_rule_planner.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_sandbox.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_schema_loader.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_semantic_builder.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_semantic_engine_catalog.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_sink_jdbc.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_user_profile.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_utils_safe_columns.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_validator.py +0 -0
- {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_writer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: satisfactoscript
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Declarative data engineering framework — multi-platform (Databricks, Snowflake, BigQuery).
|
|
5
5
|
Author-email: julhouba <houbartjulien80@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Operating System :: OS Independent
|
|
19
|
-
Classifier: Development Status ::
|
|
19
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
20
20
|
Classifier: Intended Audience :: Developers
|
|
21
21
|
Classifier: Intended Audience :: Science/Research
|
|
22
22
|
Classifier: Topic :: Database
|
|
@@ -43,6 +43,9 @@ Requires-Dist: google-generativeai>=0.5.0; extra == "llm-google"
|
|
|
43
43
|
Provides-Extra: semantic-pdf
|
|
44
44
|
Requires-Dist: fpdf2>=2.7.0; extra == "semantic-pdf"
|
|
45
45
|
Requires-Dist: matplotlib>=3.7.0; extra == "semantic-pdf"
|
|
46
|
+
Provides-Extra: serving
|
|
47
|
+
Requires-Dist: mlflow<3,>=2.12.0; extra == "serving"
|
|
48
|
+
Requires-Dist: openai>=1.0.0; extra == "serving"
|
|
46
49
|
Provides-Extra: semantic-full
|
|
47
50
|
Requires-Dist: openai>=1.0.0; extra == "semantic-full"
|
|
48
51
|
Requires-Dist: anthropic>=0.30.0; extra == "semantic-full"
|
|
@@ -54,7 +57,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
54
57
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
55
58
|
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
|
|
56
59
|
|
|
57
|
-
# SatisfactoScript Framework (v1.0.0
|
|
60
|
+
# SatisfactoScript Framework (v1.0.0)
|
|
58
61
|
|
|
59
62
|
> **An Enterprise-Ready, Declarative Data Engineering Framework for Databricks Lakehouse.**
|
|
60
63
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "satisfactoscript"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Declarative data engineering framework — multi-platform (Databricks, Snowflake, BigQuery)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
"Programming Language :: Python :: 3.11",
|
|
29
29
|
"Programming Language :: Python :: 3.12",
|
|
30
30
|
"Operating System :: OS Independent",
|
|
31
|
-
"Development Status ::
|
|
31
|
+
"Development Status :: 5 - Production/Stable",
|
|
32
32
|
"Intended Audience :: Developers",
|
|
33
33
|
"Intended Audience :: Science/Research",
|
|
34
34
|
"Topic :: Database",
|
|
@@ -74,6 +74,12 @@ semantic-pdf = [
|
|
|
74
74
|
"matplotlib>=3.7.0"
|
|
75
75
|
]
|
|
76
76
|
|
|
77
|
+
# --- Databricks Model Serving ---
|
|
78
|
+
serving = [
|
|
79
|
+
"mlflow>=2.12.0,<3",
|
|
80
|
+
"openai>=1.0.0",
|
|
81
|
+
]
|
|
82
|
+
|
|
77
83
|
# --- Bundles pratiques ---
|
|
78
84
|
semantic-full = [
|
|
79
85
|
"openai>=1.0.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .core.core import SatisfactoEngine
|
|
2
2
|
from .core.registry import RuleRegistry
|
|
3
3
|
from .core.config import ConfigurationManager
|
|
4
|
-
from .utils import safe_columns, setup_notebook
|
|
4
|
+
from .utils import safe_columns, setup_notebook, configure_logging
|
|
5
5
|
from .core.schema_loader import load_schema, parse_schema
|
|
6
6
|
from .core.rule_analyzer import RuleAnalyzer
|
|
7
7
|
from .core.rule_planner import RulePlanner, RuleCycleError
|
|
@@ -16,6 +16,7 @@ __all__ = [
|
|
|
16
16
|
"ConfigurationManager",
|
|
17
17
|
"safe_columns",
|
|
18
18
|
"setup_notebook",
|
|
19
|
+
"configure_logging",
|
|
19
20
|
"load_schema",
|
|
20
21
|
"parse_schema",
|
|
21
22
|
"RuleAnalyzer",
|
{satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/snowpark.py
RENAMED
|
@@ -19,6 +19,9 @@ import logging
|
|
|
19
19
|
from functools import reduce
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
|
+
from satisfactoscript.backends.sql_base import ANTI_SEMI_JOIN_UNSUPPORTED_MSG
|
|
23
|
+
from satisfactoscript.core.ir import _normalise_join_type
|
|
24
|
+
|
|
22
25
|
logger = logging.getLogger(__name__)
|
|
23
26
|
|
|
24
27
|
|
|
@@ -368,6 +371,9 @@ class SnowparkBackend:
|
|
|
368
371
|
return df.filter(condition)
|
|
369
372
|
|
|
370
373
|
def join(self, left: Any, right: Any, on: Any, how: str = "left") -> Any:
|
|
374
|
+
join_type = _normalise_join_type(how)
|
|
375
|
+
if join_type in {"left_anti", "left_semi"}:
|
|
376
|
+
raise NotImplementedError(ANTI_SEMI_JOIN_UNSUPPORTED_MSG)
|
|
371
377
|
return left.join(right, on=on, how=how)
|
|
372
378
|
|
|
373
379
|
def drop_columns(self, df: Any, columns: list) -> Any:
|
{satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/sql_base.py
RENAMED
|
@@ -30,6 +30,11 @@ from typing import Any
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
31
31
|
|
|
32
32
|
_alias_counter = itertools.count(1)
|
|
33
|
+
ANTI_SEMI_JOIN_UNSUPPORTED_MSG = (
|
|
34
|
+
"anti/semi join only supported on the Spark DataFrame backend so far "
|
|
35
|
+
"(see Plan 21)"
|
|
36
|
+
)
|
|
37
|
+
_UNSUPPORTED_SQL_JOIN_TYPES = {"LEFT ANTI", "LEFT SEMI"}
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
def _next_alias() -> str:
|
|
@@ -342,6 +347,8 @@ class SQLQuery:
|
|
|
342
347
|
right_inner = right_q._base_to_sql()
|
|
343
348
|
on_sql = _build_join_on(self._alias, right_q._alias, on_cond)
|
|
344
349
|
jt = _normalise_join_type(how)
|
|
350
|
+
if jt in _UNSUPPORTED_SQL_JOIN_TYPES:
|
|
351
|
+
raise NotImplementedError(ANTI_SEMI_JOIN_UNSUPPORTED_MSG)
|
|
345
352
|
parts.append(f"{jt} JOIN ({right_inner}) AS `{right_q._alias}`")
|
|
346
353
|
parts.append(f" ON {on_sql}")
|
|
347
354
|
else:
|
|
@@ -409,6 +416,10 @@ def _normalise_join_type(how: str) -> str:
|
|
|
409
416
|
how = how.upper().replace("_", " ")
|
|
410
417
|
if how in ("LEFT", "LEFT OUTER"):
|
|
411
418
|
return "LEFT"
|
|
419
|
+
if how in ("ANTI", "LEFT ANTI"):
|
|
420
|
+
return "LEFT ANTI"
|
|
421
|
+
if how in ("SEMI", "LEFT SEMI"):
|
|
422
|
+
return "LEFT SEMI"
|
|
412
423
|
if how in ("RIGHT", "RIGHT OUTER"):
|
|
413
424
|
return "RIGHT"
|
|
414
425
|
if how in ("INNER",):
|
|
@@ -639,14 +650,22 @@ class SQLBackend:
|
|
|
639
650
|
inner_op_name = resolve_filter_operator(raw_op_name) or raw_op_name
|
|
640
651
|
inner_val = parts[1] if len(parts) > 1 else None
|
|
641
652
|
# Evaluate condition against c_expr directly (not via build_filter column lookup)
|
|
642
|
-
if inner_op_name == "is_not_null":
|
|
643
|
-
|
|
644
|
-
if inner_op_name == "
|
|
645
|
-
|
|
646
|
-
if inner_op_name == "
|
|
647
|
-
|
|
648
|
-
if inner_op_name == "
|
|
649
|
-
|
|
653
|
+
if inner_op_name == "is_not_null":
|
|
654
|
+
return SQLCondition(f"{c_expr} IS NOT NULL")
|
|
655
|
+
if inner_op_name == "is_null":
|
|
656
|
+
return SQLCondition(f"{c_expr} IS NULL")
|
|
657
|
+
if inner_op_name == "contains":
|
|
658
|
+
return SQLCondition(f"{c_expr} LIKE '%{_sql_escape(str(inner_val))}%'")
|
|
659
|
+
if inner_op_name == "not_contains":
|
|
660
|
+
return SQLCondition(f"{c_expr} NOT LIKE '%{_sql_escape(str(inner_val))}%'")
|
|
661
|
+
if inner_op_name == "starts_with":
|
|
662
|
+
return SQLCondition(f"{c_expr} LIKE '{_sql_escape(str(inner_val))}%'")
|
|
663
|
+
if inner_op_name == "ends_with":
|
|
664
|
+
return SQLCondition(f"{c_expr} LIKE '%{_sql_escape(str(inner_val))}'")
|
|
665
|
+
if inner_op_name == "like":
|
|
666
|
+
return SQLCondition(f"{c_expr} LIKE '{_sql_escape(str(inner_val))}'")
|
|
667
|
+
if inner_op_name == "not_like":
|
|
668
|
+
return SQLCondition(f"{c_expr} NOT LIKE '{_sql_escape(str(inner_val))}'")
|
|
650
669
|
_comp_sym = {"equals": "=", "not_equals": "!=", "greater_than": ">",
|
|
651
670
|
"less_than": "<", "greater_than_equal": ">=", "less_than_equal": "<="}
|
|
652
671
|
if inner_op_name in _comp_sym:
|
|
@@ -675,6 +694,7 @@ class SQLBackend:
|
|
|
675
694
|
"trim": lambda: SQLColumn(f"TRIM({c_expr})"),
|
|
676
695
|
"round": lambda: SQLColumn(f"ROUND({c_expr}, {op.args[0]})"),
|
|
677
696
|
"abs": lambda: SQLColumn(f"ABS({c_expr})"),
|
|
697
|
+
"ceil": lambda: SQLColumn(f"CEIL({c_expr})"),
|
|
678
698
|
"length": lambda: SQLColumn(f"LENGTH({c_expr})"),
|
|
679
699
|
"to_date": lambda: SQLColumn(f"PARSE_DATE('{op.args[0]}', {c_expr})"),
|
|
680
700
|
"split": lambda: SQLColumn(f"SPLIT({c_expr}, '{op.args[0]}')[ORDINAL({int(op.args[1]) + 1})]"),
|
|
@@ -702,22 +722,42 @@ class SQLBackend:
|
|
|
702
722
|
val = f.value
|
|
703
723
|
c_expr = self._q(col_name)
|
|
704
724
|
|
|
705
|
-
if op == "is_not_null":
|
|
706
|
-
|
|
725
|
+
if op == "is_not_null":
|
|
726
|
+
return SQLCondition(f"{c_expr} IS NOT NULL")
|
|
727
|
+
if op == "is_null":
|
|
728
|
+
return SQLCondition(f"{c_expr} IS NULL")
|
|
707
729
|
if op in ("in", "not_in"):
|
|
708
730
|
lst = val if isinstance(val, list) else [v.strip() for v in str(val).replace(";", ",").split(",")]
|
|
709
731
|
vals_str = ", ".join(_smart_val(v) for v in lst)
|
|
710
732
|
return SQLCondition(f"{c_expr} {'NOT ' if op == 'not_in' else ''}IN ({vals_str})")
|
|
711
|
-
if op == "contains":
|
|
712
|
-
|
|
713
|
-
if op == "
|
|
714
|
-
|
|
715
|
-
if op == "
|
|
716
|
-
|
|
733
|
+
if op == "contains":
|
|
734
|
+
return SQLCondition(f"{c_expr} LIKE '%{_sql_escape(str(val))}%'")
|
|
735
|
+
if op == "not_contains":
|
|
736
|
+
return SQLCondition(f"{c_expr} NOT LIKE '%{_sql_escape(str(val))}%'")
|
|
737
|
+
if op == "starts_with":
|
|
738
|
+
return SQLCondition(f"{c_expr} LIKE '{_sql_escape(str(val))}%'")
|
|
739
|
+
if op == "ends_with":
|
|
740
|
+
return SQLCondition(f"{c_expr} LIKE '%{_sql_escape(str(val))}'")
|
|
741
|
+
if op == "like":
|
|
742
|
+
return SQLCondition(f"{c_expr} LIKE '{_sql_escape(str(val))}'")
|
|
743
|
+
if op == "not_like":
|
|
744
|
+
return SQLCondition(f"{c_expr} NOT LIKE '{_sql_escape(str(val))}'")
|
|
717
745
|
if op == "sql":
|
|
718
746
|
if not allow_raw_sql:
|
|
719
747
|
raise ValueError("[Governance] sql: filter disabled (allow_raw_sql: false).")
|
|
720
748
|
return SQLCondition(val)
|
|
749
|
+
if op == "between":
|
|
750
|
+
parts = [v.strip() for v in str(val).split(",")]
|
|
751
|
+
if len(parts) != 2:
|
|
752
|
+
raise ValueError(f"between filter on column '{col_name}' expects exactly 2 values.")
|
|
753
|
+
lo, hi = parts
|
|
754
|
+
return SQLCondition(f"({c_expr} >= {_smart_val(lo)} AND {c_expr} <= {_smart_val(hi)})")
|
|
755
|
+
if op == "not_between":
|
|
756
|
+
parts = [v.strip() for v in str(val).split(",")]
|
|
757
|
+
if len(parts) != 2:
|
|
758
|
+
raise ValueError(f"not_between filter on column '{col_name}' expects exactly 2 values.")
|
|
759
|
+
lo, hi = parts
|
|
760
|
+
return SQLCondition(f"({c_expr} < {_smart_val(lo)} OR {c_expr} > {_smart_val(hi)})")
|
|
721
761
|
_comp = {"equals": "=", "not_equals": "!=", "greater_than": ">",
|
|
722
762
|
"less_than": "<", "greater_than_equal": ">=", "less_than_equal": "<="}
|
|
723
763
|
if op in _comp:
|
|
@@ -893,4 +933,4 @@ class SQLBackend:
|
|
|
893
933
|
|
|
894
934
|
def optimize_table(self, fqn: str, zorder_cols: list[str] | None = None) -> None:
|
|
895
935
|
"""Default no-op. Override in subclasses for platform-specific optimisation."""
|
|
896
|
-
print(f" [SQLBackend] optimize_table: no-op for {fqn}")
|
|
936
|
+
print(f" [SQLBackend] optimize_table: no-op for {fqn}")
|
|
@@ -117,7 +117,6 @@ def _run_hub(args: argparse.Namespace) -> None:
|
|
|
117
117
|
"""Boucle REPL principale du SatisfactoHub."""
|
|
118
118
|
try:
|
|
119
119
|
from rich.console import Console
|
|
120
|
-
from rich.table import Table as RichTable
|
|
121
120
|
except ImportError:
|
|
122
121
|
print("[ERROR] Le module 'rich' est requis pour le hub. Installez-le avec : pip install rich")
|
|
123
122
|
sys.exit(1)
|
|
@@ -175,7 +174,7 @@ def _run_hub(args: argparse.Namespace) -> None:
|
|
|
175
174
|
|
|
176
175
|
if not args.new_session and len(session) > 0:
|
|
177
176
|
ts = session.created_at.strftime("%Y-%m-%d") if hasattr(session.created_at, "strftime") else str(session.created_at)
|
|
178
|
-
console.print(
|
|
177
|
+
console.print(" [1] Nouvelle session (efface la session précédente)")
|
|
179
178
|
console.print(f" [2] Reprendre la session du {ts} ({len(session)} messages)\n")
|
|
180
179
|
try:
|
|
181
180
|
choice = input("Choix [1/2] : ").strip()
|
|
@@ -255,8 +254,8 @@ def _run_hub(args: argparse.Namespace) -> None:
|
|
|
255
254
|
session.save(scope_id)
|
|
256
255
|
|
|
257
256
|
|
|
258
|
-
def _render_response(console:
|
|
259
|
-
"""
|
|
257
|
+
def _render_response(console: object, response: object) -> None:
|
|
258
|
+
"""Render a HubResponse in the terminal with Rich formatting."""
|
|
260
259
|
from .agentic.models import (
|
|
261
260
|
AgentResponse,
|
|
262
261
|
BuilderResponse,
|
|
@@ -264,6 +263,7 @@ def _render_response(console: "Console", response: object) -> None: # type: ign
|
|
|
264
263
|
LineageResponse,
|
|
265
264
|
QualityResponse,
|
|
266
265
|
)
|
|
266
|
+
from .serving._response_serializer import hub_response_to_text
|
|
267
267
|
|
|
268
268
|
if isinstance(response, AgentResponse):
|
|
269
269
|
if response.mode == "error":
|
|
@@ -271,27 +271,12 @@ def _render_response(console: "Console", response: object) -> None: # type: ign
|
|
|
271
271
|
elif response.mode in ("ambiguous", "needs_clarification"):
|
|
272
272
|
console.print(f"[yellow]{response.clarification_question or response.explanation}[/yellow]")
|
|
273
273
|
else:
|
|
274
|
-
|
|
274
|
+
_render_agent_response_rich(console, response)
|
|
275
275
|
|
|
276
|
-
elif isinstance(response, LineageResponse):
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
console.print(response.diagram)
|
|
281
|
-
else:
|
|
282
|
-
console.print(response.narrative or f"Lignage ({response.mode}) — colonne : {response.column}")
|
|
283
|
-
|
|
284
|
-
elif isinstance(response, QualityResponse):
|
|
285
|
-
if response.error:
|
|
286
|
-
console.print(f"[red]{response.error}[/red]")
|
|
287
|
-
else:
|
|
288
|
-
console.print(response.text_output or response.narrative or "Checks exécutés.")
|
|
289
|
-
|
|
290
|
-
elif isinstance(response, DictionaryResponse):
|
|
291
|
-
if response.error:
|
|
292
|
-
console.print(f"[red]{response.error}[/red]")
|
|
293
|
-
else:
|
|
294
|
-
console.print(response.text_output or response.narrative or f"Champ : {response.column}")
|
|
276
|
+
elif isinstance(response, (LineageResponse, QualityResponse, DictionaryResponse)):
|
|
277
|
+
text = hub_response_to_text(response)
|
|
278
|
+
style = "red" if getattr(response, "error", None) else ""
|
|
279
|
+
console.print(f"[{style}]{text}[/{style}]" if style else text)
|
|
295
280
|
|
|
296
281
|
elif isinstance(response, BuilderResponse):
|
|
297
282
|
if not response.success:
|
|
@@ -305,8 +290,8 @@ def _render_response(console: "Console", response: object) -> None: # type: ign
|
|
|
305
290
|
console.print(str(response))
|
|
306
291
|
|
|
307
292
|
|
|
308
|
-
def
|
|
309
|
-
"""
|
|
293
|
+
def _render_agent_response_rich(console: object, response: object) -> None:
|
|
294
|
+
"""Render an AgentResponse with Rich formatting (tables, colours)."""
|
|
310
295
|
try:
|
|
311
296
|
from rich.table import Table as RichTable
|
|
312
297
|
from .agentic.models import ResponseFormat
|
|
@@ -324,7 +309,6 @@ def _render_agent_response(console: "Console", response: object) -> None: # typ
|
|
|
324
309
|
console.print(f" {result.kpi_label or ''}: [green]{result.kpi_value}[/green]")
|
|
325
310
|
|
|
326
311
|
elif fmt == ResponseFormat.TABLE:
|
|
327
|
-
# Essayer d'afficher via rich.Table si le DataFrame est disponible
|
|
328
312
|
data_df = getattr(result, "data", None)
|
|
329
313
|
if data_df is not None:
|
|
330
314
|
try:
|
{satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/interpreter.py
RENAMED
|
@@ -15,6 +15,7 @@ import logging
|
|
|
15
15
|
from functools import reduce
|
|
16
16
|
from typing import TYPE_CHECKING, Any
|
|
17
17
|
|
|
18
|
+
from satisfactoscript.core.ir import _normalise_join_type
|
|
18
19
|
from satisfactoscript.core.registry import RuleRegistry
|
|
19
20
|
from satisfactoscript.core.sandbox import SandboxResolver
|
|
20
21
|
from satisfactoscript.core.rule_analyzer import RuleAnalyzer
|
|
@@ -26,6 +27,8 @@ if TYPE_CHECKING:
|
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
30
|
+
_LEFT_ONLY_JOIN_TYPES = {"left_anti", "left_semi"}
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
class SchemaInterpreter:
|
|
31
34
|
"""
|
|
@@ -340,7 +343,7 @@ class SchemaInterpreter:
|
|
|
340
343
|
|
|
341
344
|
on_l = key_l if isinstance(key_l, list) else [key_l]
|
|
342
345
|
on_r = key_r if isinstance(key_r, list) else [key_r]
|
|
343
|
-
join_type = j.get("type", "left")
|
|
346
|
+
join_type = _normalise_join_type(j.get("type", "left"))
|
|
344
347
|
|
|
345
348
|
logger.info(" -> [Join] %s JOIN %s(%s) -> %s(%s)", join_type.upper(), alias_l, on_l, alias_r, on_r)
|
|
346
349
|
df_to = dfs[alias_r]
|
|
@@ -350,7 +353,8 @@ class SchemaInterpreter:
|
|
|
350
353
|
else:
|
|
351
354
|
cond = reduce(lambda x, y: x & y, [df_main[lk] == df_to[rk] for lk, rk in zip(on_l, on_r)])
|
|
352
355
|
df_main = b.join(df_main, df_to, cond, join_type)
|
|
353
|
-
|
|
356
|
+
if join_type not in _LEFT_ONLY_JOIN_TYPES:
|
|
357
|
+
df_main = b.drop_columns(df_main, [df_to[r] for r in on_r])
|
|
354
358
|
|
|
355
359
|
# 3. BUSINESS RULES
|
|
356
360
|
if "business_rules" in schema_dict:
|
|
@@ -11,6 +11,29 @@ from __future__ import annotations
|
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
VALID_JOIN_TYPES: frozenset[str] = frozenset(
|
|
15
|
+
{"left", "right", "inner", "full", "cross", "left_anti", "left_semi"}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_JOIN_TYPE_ALIASES: dict[str, str] = {
|
|
19
|
+
"left outer": "left",
|
|
20
|
+
"right outer": "right",
|
|
21
|
+
"outer": "full",
|
|
22
|
+
"full outer": "full",
|
|
23
|
+
"full_outer": "full",
|
|
24
|
+
"anti": "left_anti",
|
|
25
|
+
"left anti": "left_anti",
|
|
26
|
+
"leftanti": "left_anti",
|
|
27
|
+
"semi": "left_semi",
|
|
28
|
+
"left semi": "left_semi",
|
|
29
|
+
"leftsemi": "left_semi",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _normalise_join_type(raw: str) -> str:
|
|
34
|
+
key = raw.strip().lower().replace("-", " ")
|
|
35
|
+
return _JOIN_TYPE_ALIASES.get(key, key)
|
|
36
|
+
|
|
14
37
|
|
|
15
38
|
# ---------------------------------------------------------------------------
|
|
16
39
|
# Leaf types
|
|
@@ -180,12 +203,19 @@ def parse_to_ir(schema_dict: dict) -> ParsedSchema:
|
|
|
180
203
|
else:
|
|
181
204
|
alias_r = table_to
|
|
182
205
|
key_r = j.get("on_to")
|
|
206
|
+
raw_type = j.get("type", "left")
|
|
207
|
+
join_type = _normalise_join_type(raw_type)
|
|
208
|
+
if join_type not in VALID_JOIN_TYPES:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
f"Invalid join type {raw_type!r} for join {alias_l!r} → {alias_r!r}. "
|
|
211
|
+
f"Valid types: {', '.join(sorted(VALID_JOIN_TYPES))}"
|
|
212
|
+
)
|
|
183
213
|
joins.append(ParsedJoin(
|
|
184
214
|
alias_left=alias_l,
|
|
185
215
|
keys_left=key_l if isinstance(key_l, list) else [key_l],
|
|
186
216
|
alias_right=alias_r,
|
|
187
217
|
keys_right=key_r if isinstance(key_r, list) else [key_r],
|
|
188
|
-
join_type=
|
|
218
|
+
join_type=join_type,
|
|
189
219
|
))
|
|
190
220
|
|
|
191
221
|
# --- select_final / add_columns ---
|
|
@@ -32,7 +32,7 @@ class OperatorSpec:
|
|
|
32
32
|
"""
|
|
33
33
|
``"none"`` — operator takes no value (``is_null``, ``is_not_null``)
|
|
34
34
|
``"single"`` — operator takes one scalar value
|
|
35
|
-
``"list"`` — operator takes a comma-separated / YAML-list value (``in``, ``not_in``)
|
|
35
|
+
``"list"`` — operator takes a comma-separated / YAML-list value (``in``, ``not_in``, ``between``, ``not_between``)
|
|
36
36
|
``"sql"`` — operator takes a raw SQL expression (``sql``)
|
|
37
37
|
"""
|
|
38
38
|
|
|
@@ -49,7 +49,7 @@ class OpSpec:
|
|
|
49
49
|
|
|
50
50
|
arity: str = "none"
|
|
51
51
|
"""
|
|
52
|
-
``"none"`` — no argument (``upper``, ``lower``, ``trim``, ``abs``, ``length``)
|
|
52
|
+
``"none"`` — no argument (``upper``, ``lower``, ``trim``, ``abs``, ``length``, ``ceil``)
|
|
53
53
|
``"single"`` — one argument after ``:`` (``cast:``, ``lit:``, ``round:``, …)
|
|
54
54
|
``"two"`` — two comma-separated arguments (``split:sep,idx``, ``substring:start,len``)
|
|
55
55
|
``"expression"`` — raw SQL expression (``expr:``)
|
|
@@ -125,6 +125,16 @@ FILTER_OPERATORS: dict[str, OperatorSpec] = {
|
|
|
125
125
|
arity="list",
|
|
126
126
|
description="Passes rows where column value is in the provided list.",
|
|
127
127
|
),
|
|
128
|
+
"between": OperatorSpec(
|
|
129
|
+
canonical="between",
|
|
130
|
+
arity="list",
|
|
131
|
+
description="Passes rows where lo <= column <= hi (inclusive). Two comma-separated values.",
|
|
132
|
+
),
|
|
133
|
+
"not_between": OperatorSpec(
|
|
134
|
+
canonical="not_between",
|
|
135
|
+
arity="list",
|
|
136
|
+
description="Passes rows where column is strictly outside the provided [lo, hi] interval.",
|
|
137
|
+
),
|
|
128
138
|
"not_in": OperatorSpec(
|
|
129
139
|
canonical="not_in",
|
|
130
140
|
arity="list",
|
|
@@ -189,6 +199,7 @@ COLUMN_OPS: dict[str, OpSpec] = {
|
|
|
189
199
|
# ---- numeric ----
|
|
190
200
|
"round": OpSpec("round", arity="single", description="Round to N decimal places."),
|
|
191
201
|
"abs": OpSpec("abs", arity="none", description="Absolute value."),
|
|
202
|
+
"ceil": OpSpec("ceil", arity="none", description="Round up to nearest integer."),
|
|
192
203
|
# ---- date ----
|
|
193
204
|
"to_date": OpSpec("to_date", arity="single", description="Parse string to date using the given format."),
|
|
194
205
|
# ---- null handling ----
|