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.
Files changed (131) hide show
  1. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/PKG-INFO +6 -3
  2. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/README.md +1 -1
  3. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/pyproject.toml +8 -2
  4. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/__init__.py +2 -1
  5. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/snowpark.py +6 -0
  6. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/sql_base.py +57 -17
  7. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/cli.py +11 -27
  8. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/interpreter.py +6 -2
  9. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/ir.py +31 -1
  10. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/op_catalog.py +13 -2
  11. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/operations.py +108 -45
  12. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/rule_executor.py +22 -5
  13. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/llm_provider.py +105 -4
  14. satisfactoscript-1.2.0/src/satisfactoscript/serving/__init__.py +20 -0
  15. satisfactoscript-1.2.0/src/satisfactoscript/serving/_response_serializer.py +96 -0
  16. satisfactoscript-1.2.0/src/satisfactoscript/serving/chat_model.py +122 -0
  17. satisfactoscript-1.2.0/src/satisfactoscript/utils.py +130 -0
  18. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/PKG-INFO +6 -3
  19. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/SOURCES.txt +6 -0
  20. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/requires.txt +4 -0
  21. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_snowpark.py +10 -0
  22. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_sql_base.py +65 -0
  23. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core.py +60 -0
  24. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core_join.py +90 -0
  25. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_ir.py +48 -0
  26. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_llm_provider.py +107 -0
  27. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_op_catalog.py +9 -1
  28. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_registry_import_paths.py +8 -0
  29. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_rule_executor.py +31 -0
  30. satisfactoscript-1.2.0/tests/test_serving_chat_model.py +147 -0
  31. satisfactoscript-1.2.0/tests/test_serving_response_serializer.py +183 -0
  32. satisfactoscript-1.2.0/tests/test_utils_logging.py +95 -0
  33. satisfactoscript-1.0.0b4/src/satisfactoscript/utils.py +0 -66
  34. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/setup.cfg +0 -0
  35. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/__init__.py +0 -0
  36. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/agent.py +0 -0
  37. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/builder_agent.py +0 -0
  38. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/dictionary_agent.py +0 -0
  39. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/exporter.py +0 -0
  40. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/history.py +0 -0
  41. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/hub.py +0 -0
  42. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/lineage_agent.py +0 -0
  43. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/models.py +0 -0
  44. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/orchestrator.py +0 -0
  45. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/quality_agent.py +0 -0
  46. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/resolver.py +0 -0
  47. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/agentic/user_profile.py +0 -0
  48. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/__init__.py +0 -0
  49. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/bigquery.py +0 -0
  50. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/backends/spark.py +0 -0
  51. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/__init__.py +0 -0
  52. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/backend.py +0 -0
  53. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/catalog_inspector.py +0 -0
  54. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/config.py +0 -0
  55. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/context.py +0 -0
  56. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/core.py +0 -0
  57. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/environment.py +0 -0
  58. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/json_schema.py +0 -0
  59. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/loaders.py +0 -0
  60. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/patterns.py +0 -0
  61. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/registry.py +0 -0
  62. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/rule_analyzer.py +0 -0
  63. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/rule_planner.py +0 -0
  64. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/sandbox.py +0 -0
  65. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/schema_loader.py +0 -0
  66. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/core/writer.py +0 -0
  67. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/lineage/__init__.py +0 -0
  68. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/lineage/dictionary.py +0 -0
  69. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/lineage/renderer.py +0 -0
  70. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/lineage/tracker.py +0 -0
  71. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/__init__.py +0 -0
  72. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/alerts.py +0 -0
  73. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/checks.py +0 -0
  74. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/contracts.py +0 -0
  75. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/history.py +0 -0
  76. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/monitor.py +0 -0
  77. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/observability/reporter.py +0 -0
  78. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/registry.py +0 -0
  79. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/__init__.py +0 -0
  80. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/builder.py +0 -0
  81. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/extractor.py +0 -0
  82. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/glossary.py +0 -0
  83. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/semantic.py +0 -0
  84. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/semantic/validator.py +0 -0
  85. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/sinks/__init__.py +0 -0
  86. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/sinks/jdbc.py +0 -0
  87. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript/spark_factory.py +0 -0
  88. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/dependency_links.txt +0 -0
  89. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/entry_points.txt +0 -0
  90. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/src/satisfactoscript.egg-info/top_level.txt +0 -0
  91. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_agent.py +0 -0
  92. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_bigquery.py +0 -0
  93. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_protocol.py +0 -0
  94. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_backend_spark.py +0 -0
  95. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_builder_agent.py +0 -0
  96. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_catalog_inspector.py +0 -0
  97. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_cli.py +0 -0
  98. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_config.py +0 -0
  99. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core_connect_patch.py +0 -0
  100. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core_env_detection.py +0 -0
  101. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_core_username.py +0 -0
  102. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_dictionary_agent.py +0 -0
  103. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_dummy.py +0 -0
  104. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_engine_fake_backend.py +0 -0
  105. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_engine_with_backend.py +0 -0
  106. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_history.py +0 -0
  107. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_hub.py +0 -0
  108. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_interpreter.py +0 -0
  109. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_json_schema.py +0 -0
  110. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_lineage_agent.py +0 -0
  111. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_lineage_dictionary.py +0 -0
  112. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_lineage_renderer.py +0 -0
  113. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_lineage_tracker.py +0 -0
  114. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_loaders.py +0 -0
  115. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_observability.py +0 -0
  116. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_orchestrator.py +0 -0
  117. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_patterns.py +0 -0
  118. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_quality_agent.py +0 -0
  119. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_registry.py +0 -0
  120. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_resolver.py +0 -0
  121. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_rule_analyzer.py +0 -0
  122. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_rule_planner.py +0 -0
  123. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_sandbox.py +0 -0
  124. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_schema_loader.py +0 -0
  125. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_semantic_builder.py +0 -0
  126. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_semantic_engine_catalog.py +0 -0
  127. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_sink_jdbc.py +0 -0
  128. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_user_profile.py +0 -0
  129. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_utils_safe_columns.py +0 -0
  130. {satisfactoscript-1.0.0b4 → satisfactoscript-1.2.0}/tests/test_validator.py +0 -0
  131. {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.0b4
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 :: 4 - Beta
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-beta.4)
60
+ # SatisfactoScript Framework (v1.0.0)
58
61
 
59
62
  > **An Enterprise-Ready, Declarative Data Engineering Framework for Databricks Lakehouse.**
60
63
 
@@ -1,4 +1,4 @@
1
- # SatisfactoScript Framework (v1.0.0-beta.4)
1
+ # SatisfactoScript Framework (v1.0.0)
2
2
 
3
3
  > **An Enterprise-Ready, Declarative Data Engineering Framework for Databricks Lakehouse.**
4
4
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "satisfactoscript"
7
- version = "1.0.0-beta.4"
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 :: 4 - Beta",
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",
@@ -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:
@@ -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": return SQLCondition(f"{c_expr} IS NOT NULL")
643
- if inner_op_name == "is_null": return SQLCondition(f"{c_expr} IS NULL")
644
- if inner_op_name == "contains": return SQLCondition(f"{c_expr} LIKE '%{_sql_escape(str(inner_val))}%'")
645
- if inner_op_name == "not_contains": return SQLCondition(f"{c_expr} NOT LIKE '%{_sql_escape(str(inner_val))}%'")
646
- if inner_op_name == "starts_with": return SQLCondition(f"{c_expr} LIKE '{_sql_escape(str(inner_val))}%'")
647
- if inner_op_name == "ends_with": return SQLCondition(f"{c_expr} LIKE '%{_sql_escape(str(inner_val))}'")
648
- if inner_op_name == "like": return SQLCondition(f"{c_expr} LIKE '{_sql_escape(str(inner_val))}'")
649
- if inner_op_name == "not_like": return SQLCondition(f"{c_expr} NOT LIKE '{_sql_escape(str(inner_val))}'")
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": return SQLCondition(f"{c_expr} IS NOT NULL")
706
- if op == "is_null": return SQLCondition(f"{c_expr} IS NULL")
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": return SQLCondition(f"{c_expr} LIKE '%{_sql_escape(str(val))}%'")
712
- if op == "not_contains": return SQLCondition(f"{c_expr} NOT LIKE '%{_sql_escape(str(val))}%'")
713
- if op == "starts_with": return SQLCondition(f"{c_expr} LIKE '{_sql_escape(str(val))}%'")
714
- if op == "ends_with": return SQLCondition(f"{c_expr} LIKE '%{_sql_escape(str(val))}'")
715
- if op == "like": return SQLCondition(f"{c_expr} LIKE '{_sql_escape(str(val))}'")
716
- if op == "not_like": return SQLCondition(f"{c_expr} NOT LIKE '{_sql_escape(str(val))}'")
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(f" [1] Nouvelle session (efface la session précédente)")
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: "Console", response: object) -> None: # type: ignore[name-defined]
259
- """Affiche la réponse dans le terminal selon son type."""
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
- _render_agent_response(console, response)
274
+ _render_agent_response_rich(console, response)
275
275
 
276
- elif isinstance(response, LineageResponse):
277
- if response.error:
278
- console.print(f"[red]{response.error}[/red]")
279
- elif response.diagram:
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 _render_agent_response(console: "Console", response: object) -> None: # type: ignore[name-defined]
309
- """Affiche un AgentResponse en mode query selon le format du résultat."""
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:
@@ -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
- df_main = b.drop_columns(df_main, [df_to[r] for r in on_r])
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=j.get("type", "left"),
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 ----