batch-analytics 0.3.13__tar.gz → 0.3.15__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 (32) hide show
  1. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/PKG-INFO +10 -1
  2. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/pyproject.toml +12 -1
  3. batch_analytics-0.3.15/src/batch_analytics/analytics/gluon_autogluon_infer.py +116 -0
  4. batch_analytics-0.3.15/src/batch_analytics/analytics/gluon_autogluon_train.py +114 -0
  5. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/config.py +13 -4
  6. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/extract.py +48 -2
  7. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/transform.py +17 -5
  8. batch_analytics-0.3.15/src/batch_analytics/utils/__init__.py +1 -0
  9. batch_analytics-0.3.15/src/batch_analytics/utils/gluon_autogluon_common.py +83 -0
  10. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics.egg-info/PKG-INFO +10 -1
  11. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics.egg-info/SOURCES.txt +5 -1
  12. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics.egg-info/requires.txt +18 -0
  13. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/README.md +0 -0
  14. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/setup.cfg +0 -0
  15. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/__init__.py +0 -0
  16. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/__main__.py +0 -0
  17. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/analytics/__init__.py +0 -0
  18. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/analytics/correlation.py +0 -0
  19. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/analytics/linear_regression.py +0 -0
  20. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/analytics/pca_clustering.py +0 -0
  21. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/analytics/t_test.py +0 -0
  22. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/job_runner.py +0 -0
  23. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/log.py +0 -0
  24. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/modules.py +0 -0
  25. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/output/__init__.py +0 -0
  26. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/output/base.py +0 -0
  27. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/output/clickhouse.py +0 -0
  28. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/output/local.py +0 -0
  29. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics/output/s3.py +0 -0
  30. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics.egg-info/dependency_links.txt +0 -0
  31. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics.egg-info/entry_points.txt +0 -0
  32. {batch_analytics-0.3.13 → batch_analytics-0.3.15}/src/batch_analytics.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batch-analytics
3
- Version: 0.3.13
3
+ Version: 0.3.15
4
4
  Summary: PySpark batch analytics: Extract, Transform, Stage, and analytical modules (linear regression, correlation, PCA, t-test).
5
5
  Author: Litewave Analytics Team
6
6
  License: MIT
@@ -22,11 +22,20 @@ Provides-Extra: output
22
22
  Requires-Dist: boto3>=1.28; extra == "output"
23
23
  Requires-Dist: clickhouse-connect<0.9,>=0.7; python_version < "3.9" and extra == "output"
24
24
  Requires-Dist: clickhouse-connect>=0.7; python_version >= "3.9" and extra == "output"
25
+ Provides-Extra: autogluon
26
+ Requires-Dist: autogluon<2.0,>=1.0; extra == "autogluon"
27
+ Requires-Dist: pandas>=1.3.0; extra == "autogluon"
28
+ Requires-Dist: boto3>=1.28; extra == "autogluon"
29
+ Requires-Dist: clickhouse-connect<0.9,>=0.7; python_version < "3.9" and extra == "autogluon"
30
+ Requires-Dist: clickhouse-connect>=0.7; python_version >= "3.9" and extra == "autogluon"
31
+ Requires-Dist: pyarrow>=10.0.0; python_version >= "3.8" and extra == "autogluon"
25
32
  Provides-Extra: full
26
33
  Requires-Dist: scipy>=1.5.0; extra == "full"
27
34
  Requires-Dist: boto3>=1.28; extra == "full"
28
35
  Requires-Dist: clickhouse-connect<0.9,>=0.7; python_version < "3.9" and extra == "full"
29
36
  Requires-Dist: clickhouse-connect>=0.7; python_version >= "3.9" and extra == "full"
37
+ Requires-Dist: autogluon<2.0,>=1.0; extra == "full"
38
+ Requires-Dist: pyarrow>=10.0.0; python_version >= "3.8" and extra == "full"
30
39
 
31
40
  # Batch Analytics
32
41
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "batch-analytics"
7
- version = "0.3.13"
7
+ version = "0.3.15"
8
8
  description = "PySpark batch analytics: Extract, Transform, Stage, and analytical modules (linear regression, correlation, PCA, t-test)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -32,12 +32,23 @@ output = [
32
32
  "clickhouse-connect>=0.7,<0.9; python_version < '3.9'",
33
33
  "clickhouse-connect>=0.7; python_version >= '3.9'",
34
34
  ]
35
+ # AutoGluon Tabular train/infer (POST /submit/train, /submit/inf on analytics_runner)
36
+ autogluon = [
37
+ "autogluon>=1.0,<2.0",
38
+ "pandas>=1.3.0",
39
+ "boto3>=1.28",
40
+ "clickhouse-connect>=0.7,<0.9; python_version < '3.9'",
41
+ "clickhouse-connect>=0.7; python_version >= '3.9'",
42
+ "pyarrow>=10.0.0; python_version >= '3.8'",
43
+ ]
35
44
  # Install all optional runtime deps used anywhere in the package
36
45
  full = [
37
46
  "scipy>=1.5.0",
38
47
  "boto3>=1.28",
39
48
  "clickhouse-connect>=0.7,<0.9; python_version < '3.9'",
40
49
  "clickhouse-connect>=0.7; python_version >= '3.9'",
50
+ "autogluon>=1.0,<2.0",
51
+ "pyarrow>=10.0.0; python_version >= '3.8'",
41
52
  ]
42
53
 
43
54
  [project.scripts]
@@ -0,0 +1,116 @@
1
+ """
2
+ Load AutoGluon TabularPredictor from S3; score ClickHouse rows; write predictions to output.
3
+
4
+ Env: CLICKHOUSE_*, BATCH_STAGING_TABLE (inference feature table), MODEL_S3_PREFIX,
5
+ OUTPUT_TYPE, OUTPUT_CLICKHOUSE_DATABASE, OUTPUT_CLICKHOUSE_TABLE (clickhouse),
6
+ or OUTPUT_S3_PATH (s3 parquet)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ import shutil
14
+ import sys
15
+ import tempfile
16
+
17
+ import clickhouse_connect
18
+ import pandas as pd
19
+
20
+ try:
21
+ from batch_analytics.utils.gluon_autogluon_common import (
22
+ clickhouse_full_table,
23
+ download_s3_prefix_to_dir,
24
+ parse_s3_uri,
25
+ )
26
+ except ImportError:
27
+ _pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
28
+ if _pkg_root not in sys.path:
29
+ sys.path.insert(0, _pkg_root)
30
+ from utils.gluon_autogluon_common import ( # noqa: E402
31
+ clickhouse_full_table,
32
+ download_s3_prefix_to_dir,
33
+ parse_s3_uri,
34
+ )
35
+
36
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def _require(name: str) -> str:
41
+ v = os.environ.get(name, "").strip()
42
+ if not v:
43
+ logger.error("Missing required env var: %s", name)
44
+ sys.exit(2)
45
+ return v
46
+
47
+
48
+ def main() -> None:
49
+ model_prefix = _require("MODEL_S3_PREFIX")
50
+ out_type = os.environ.get("OUTPUT_TYPE", "clickhouse").strip().lower()
51
+ if out_type not in ("clickhouse", "s3"):
52
+ logger.error("OUTPUT_TYPE must be clickhouse or s3, got %r", out_type)
53
+ sys.exit(2)
54
+
55
+ host = _require("CLICKHOUSE_HOST")
56
+ port = int(os.environ.get("CLICKHOUSE_HTTP_PORT", "8123"))
57
+ database = os.environ.get("CLICKHOUSE_DB", "default").strip() or "default"
58
+ user = os.environ.get("CLICKHOUSE_USER", "default")
59
+ password = os.environ.get("CLICKHOUSE_PASSWORD", "")
60
+ inference_table = _require("BATCH_STAGING_TABLE")
61
+ full_table = clickhouse_full_table(database, inference_table)
62
+
63
+ sql = f"SELECT * FROM {full_table}"
64
+ logger.info("Loading rows to score: %s", sql)
65
+ client = clickhouse_connect.get_client(
66
+ host=host,
67
+ port=port,
68
+ username=user,
69
+ password=password or None,
70
+ database=database,
71
+ )
72
+ df = client.query_df(sql)
73
+ if df.empty:
74
+ logger.warning("Inference input is empty; nothing to write")
75
+ return
76
+
77
+ try:
78
+ from autogluon.tabular import TabularPredictor
79
+ except ImportError:
80
+ logger.exception("autogluon is not installed; use pip install 'batch-analytics[autogluon]'")
81
+ sys.exit(4)
82
+
83
+ local_model = tempfile.mkdtemp(prefix="ag_infer_")
84
+ try:
85
+ logger.info("Downloading model from %s", model_prefix)
86
+ download_s3_prefix_to_dir(model_prefix, local_model)
87
+ predictor = TabularPredictor.load(local_model)
88
+ y_pred = predictor.predict(df)
89
+ out_df = df.copy()
90
+ out_df["prediction"] = pd.Series(y_pred, index=df.index)
91
+
92
+ if out_type == "clickhouse":
93
+ odb = _require("OUTPUT_CLICKHOUSE_DATABASE")
94
+ otbl = _require("OUTPUT_CLICKHOUSE_TABLE")
95
+ out_full = clickhouse_full_table(odb, otbl)
96
+ logger.info("Inserting %s rows into %s", len(out_df), out_full)
97
+ client.insert_df(out_full, out_df)
98
+ else:
99
+ path = _require("OUTPUT_S3_PATH")
100
+ bucket, key = parse_s3_uri(path.rstrip("/") + "/")
101
+ key = key.rstrip("/")
102
+ if key:
103
+ key = key + "/"
104
+ parquet_key = key + "predictions.parquet"
105
+ tmp_parquet = os.path.join(local_model, "predictions.parquet")
106
+ out_df.to_parquet(tmp_parquet, index=False)
107
+ import boto3
108
+
109
+ boto3.client("s3").upload_file(tmp_parquet, bucket, parquet_key)
110
+ logger.info("Wrote s3://%s/%s", bucket, parquet_key)
111
+ finally:
112
+ shutil.rmtree(local_model, ignore_errors=True)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,114 @@
1
+ """
2
+ Train AutoGluon TabularPredictor from ClickHouse staging data; upload artifacts to S3.
3
+
4
+ Env (injected by analytics_runner Gluon job):
5
+ CLICKHOUSE_*, BATCH_STAGING_TABLE, MODEL_S3_PREFIX, TASK_ID,
6
+ AUTOGLUON_LABEL, AUTOGLUON_FEATURES, AUTOGLUON_PROBLEM_TYPE, AUTOGLUON_TIME_LIMIT,
7
+ optional AUTOGLUON_MAX_ROWS
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ import shutil
15
+ import sys
16
+
17
+ import clickhouse_connect
18
+
19
+ try:
20
+ from batch_analytics.utils.gluon_autogluon_common import (
21
+ clickhouse_full_table,
22
+ local_training_dir,
23
+ upload_directory_to_s3,
24
+ )
25
+ except ImportError:
26
+ _pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
27
+ if _pkg_root not in sys.path:
28
+ sys.path.insert(0, _pkg_root)
29
+ from utils.gluon_autogluon_common import ( # noqa: E402
30
+ clickhouse_full_table,
31
+ local_training_dir,
32
+ upload_directory_to_s3,
33
+ )
34
+
35
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def _require(name: str) -> str:
40
+ v = os.environ.get(name, "").strip()
41
+ if not v:
42
+ logger.error("Missing required env var: %s", name)
43
+ sys.exit(2)
44
+ return v
45
+
46
+
47
+ def main() -> None:
48
+ model_prefix = _require("MODEL_S3_PREFIX")
49
+ label = _require("AUTOGLUON_LABEL")
50
+ features_raw = _require("AUTOGLUON_FEATURES")
51
+ feature_list = [c.strip() for c in features_raw.split(",") if c.strip()]
52
+
53
+ host = _require("CLICKHOUSE_HOST")
54
+ port = int(os.environ.get("CLICKHOUSE_HTTP_PORT", "8123"))
55
+ database = os.environ.get("CLICKHOUSE_DB", "default").strip() or "default"
56
+ user = os.environ.get("CLICKHOUSE_USER", "default")
57
+ password = os.environ.get("CLICKHOUSE_PASSWORD", "")
58
+ staging_table = _require("BATCH_STAGING_TABLE")
59
+ full_table = clickhouse_full_table(database, staging_table)
60
+
61
+ max_rows = os.environ.get("AUTOGLUON_MAX_ROWS", "").strip()
62
+ limit_sql = f" LIMIT {int(max_rows)}" if max_rows else ""
63
+ sql = f"SELECT * FROM {full_table}{limit_sql}"
64
+
65
+ logger.info("Loading training data: %s", sql)
66
+ client = clickhouse_connect.get_client(
67
+ host=host,
68
+ port=port,
69
+ username=user,
70
+ password=password or None,
71
+ database=database,
72
+ )
73
+ df = client.query_df(sql)
74
+
75
+ missing = [c for c in feature_list + [label] if c not in df.columns]
76
+ if missing:
77
+ logger.error("Columns missing from training data: %s (have: %s)", missing, list(df.columns))
78
+ sys.exit(3)
79
+
80
+ try:
81
+ from autogluon.tabular import TabularPredictor
82
+ except ImportError:
83
+ logger.exception("autogluon is not installed; use pip install 'batch-analytics[autogluon]'")
84
+ sys.exit(4)
85
+
86
+ problem_type = os.environ.get("AUTOGLUON_PROBLEM_TYPE", "binary").strip() or "binary"
87
+ time_limit = int(os.environ.get("AUTOGLUON_TIME_LIMIT", "300"))
88
+
89
+ local_dir = local_training_dir()
90
+ if os.path.isdir(local_dir):
91
+ shutil.rmtree(local_dir)
92
+ os.makedirs(local_dir, exist_ok=True)
93
+
94
+ train_df = df[feature_list + [label]]
95
+ logger.info(
96
+ "Fitting TabularPredictor problem_type=%s time_limit=%ss rows=%s",
97
+ problem_type,
98
+ time_limit,
99
+ len(train_df),
100
+ )
101
+ predictor = TabularPredictor(
102
+ label=label,
103
+ problem_type=problem_type,
104
+ path=local_dir,
105
+ )
106
+ predictor.fit(train_df, time_limit=time_limit)
107
+
108
+ logger.info("Uploading model artifacts to %s", model_prefix)
109
+ upload_directory_to_s3(local_dir, model_prefix)
110
+ logger.info("Train finished; task_id=%s", os.environ.get("TASK_ID", ""))
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
@@ -29,16 +29,23 @@ class ClickHouseConfig:
29
29
 
30
30
  @property
31
31
  def jdbc_properties(self) -> dict:
32
+ """JDBC connection properties for Spark.
33
+
34
+ clickhouse-jdbc (clickhouse-java v0.6+) rejects legacy keys such as
35
+ ``compress_algorithm`` (ClientMisconfigurationException). Prefer JDBC URL
36
+ query parameters for compression behavior. To force the old property for
37
+ legacy stacks, set CLICKHOUSE_JDBC_LEGACY_COMPRESS_ALGORITHM (e.g. ``none``).
38
+ """
32
39
  props = {
33
40
  "user": self.user,
34
41
  "driver": "com.clickhouse.jdbc.ClickHouseDriver",
35
- # Match Spark read codec default: avoids JDBC LZ4/gzip mismatches with server HTTP compression
36
- "compress_algorithm": os.environ.get(
37
- "CLICKHOUSE_JDBC_COMPRESS_ALGORITHM", "none"
38
- ),
39
42
  }
40
43
  if self.password:
41
44
  props["password"] = self.password
45
+ # Opt-in legacy property for older shaded JDBC stacks only.
46
+ legacy = os.environ.get("CLICKHOUSE_JDBC_LEGACY_COMPRESS_ALGORITHM", "").strip()
47
+ if legacy:
48
+ props["compress_algorithm"] = legacy
42
49
  return props
43
50
 
44
51
 
@@ -78,6 +85,8 @@ class TransformConfig:
78
85
  staging_format: str = os.environ.get("BATCH_STAGING_FORMAT", "clickhouse")
79
86
  # Staging table name in ClickHouse (when format=clickhouse)
80
87
  staging_table: str = os.environ.get("BATCH_STAGING_TABLE", "analytics_staging")
88
+ # Spark save mode for ClickHouse staging (and path staging): overwrite | append
89
+ staging_write_mode: str = os.environ.get("BATCH_STAGING_WRITE_MODE", "overwrite")
81
90
  # Source column holding a JSON object or Python dict string; every top-level key becomes a new String column
82
91
  # (see transform.expand_kv_blob_column). Example: add_dimensions {'anchor_id':'...','lot':'A1'}
83
92
  add_dimension_column: str = os.environ.get("BATCH_ADD_DIMENSION_COLUMN", "add_dimension")
@@ -4,7 +4,8 @@ Extract stage: Load data from ClickHouse using Spark ClickHouse connector or JDB
4
4
 
5
5
  import json
6
6
  import logging
7
- from typing import Dict, List, Optional
7
+ import os
8
+ from typing import Dict, List, Optional, Tuple
8
9
 
9
10
  from pyspark.sql import DataFrame, SparkSession
10
11
  from pyspark.sql.functions import col
@@ -59,6 +60,48 @@ def _apply_extract_filter(df: DataFrame, config: BatchAnalyticsConfig) -> DataFr
59
60
  return filtered
60
61
 
61
62
 
63
+ def _parse_ch_database_table(table: str, default_database: str) -> Tuple[str, str]:
64
+ """
65
+ Resolve ``table`` reference to (database, table_name).
66
+
67
+ ``batch_metric_facts`` → (default_database, batch_metric_facts)
68
+ ``analytics.batch_metric_facts`` → (analytics, batch_metric_facts)
69
+ """
70
+ t = (table or "").strip()
71
+ if not t:
72
+ return default_database, t
73
+ if "." in t and not t.startswith("("):
74
+ db, tbl = t.split(".", 1)
75
+ db, tbl = db.strip(), tbl.strip()
76
+ if db and tbl:
77
+ return db, tbl
78
+ return default_database, t
79
+
80
+
81
+ def _read_via_catalog(spark: SparkSession, cfg: BatchAnalyticsConfig, table: str) -> Optional[DataFrame]:
82
+ """
83
+ Read via Spark SQL catalog (ClickHouseCatalog), registered in job_runner.create_spark_session.
84
+
85
+ clickhouse-spark-runtime does **not** register legacy short name ``format(\"clickhouse\")`` /
86
+ ``clickhouse.DefaultSource``; catalog + ``spark.table(catalog.db.table)`` is the supported path.
87
+ """
88
+ cat = os.environ.get("BATCH_CLICKHOUSE_CATALOG", "batch_ch").strip()
89
+ if not cat:
90
+ return None
91
+ db, tbl = _parse_ch_database_table(table, cfg.clickhouse.database)
92
+ ident = f"{cat}.{db}.{tbl}"
93
+ try:
94
+ return spark.table(ident)
95
+ except Exception as e:
96
+ logger.warning(
97
+ "Catalog read failed for %s (%s): %s. Trying other readers.",
98
+ ident,
99
+ table,
100
+ e,
101
+ )
102
+ return None
103
+
104
+
62
105
  def _read_via_format(spark: SparkSession, cfg: BatchAnalyticsConfig, table: str) -> Optional[DataFrame]:
63
106
  """
64
107
  Read from ClickHouse using the native format API (clickhouse-spark-runtime).
@@ -106,7 +149,10 @@ def extract_table(
106
149
  Uses native connector if configured, otherwise JDBC.
107
150
  """
108
151
  if config.extract.use_native_connector:
109
- df = _read_via_format(spark, config, table)
152
+ # Prefer catalog (matches clickhouse-spark-runtime); avoid legacy DefaultSource path.
153
+ df = _read_via_catalog(spark, config, table)
154
+ if df is None:
155
+ df = _read_via_format(spark, config, table)
110
156
  if df is None:
111
157
  df = _read_via_jdbc(spark, config, table)
112
158
  else:
@@ -183,6 +183,15 @@ def transform(
183
183
  return remove_duplicates(transformed, key_columns=dedup_cols)
184
184
 
185
185
 
186
+ def _normalize_staging_write_mode(raw: str) -> str:
187
+ """Spark DataFrameWriter mode: overwrite (replace table contents) or append."""
188
+ m = (raw or "overwrite").strip().lower()
189
+ if m in ("overwrite", "append"):
190
+ return m
191
+ logger.warning("Invalid BATCH_STAGING_WRITE_MODE=%r; using overwrite", raw)
192
+ return "overwrite"
193
+
194
+
186
195
  def stage_to_clickhouse(
187
196
  spark: SparkSession,
188
197
  df: DataFrame,
@@ -192,8 +201,10 @@ def stage_to_clickhouse(
192
201
  Write transformed data to ClickHouse staging table.
193
202
  Separate job from transform; must complete before analytics can run.
194
203
  Uses native connector if available, else JDBC.
204
+ Write mode from BATCH_STAGING_WRITE_MODE (default overwrite = full replace).
195
205
  """
196
206
  n = df.count()
207
+ mode = _normalize_staging_write_mode(config.transform.staging_write_mode)
197
208
  try:
198
209
  ch = config.clickhouse
199
210
  writer = (
@@ -204,7 +215,7 @@ def stage_to_clickhouse(
204
215
  .option("database", ch.database)
205
216
  .option("table", config.transform.staging_table)
206
217
  .option("user", ch.user)
207
- .mode("overwrite")
218
+ .mode(mode)
208
219
  )
209
220
  if ch.password:
210
221
  writer = writer.option("password", ch.password)
@@ -214,7 +225,7 @@ def stage_to_clickhouse(
214
225
  df.write.jdbc(
215
226
  config.clickhouse.jdbc_url,
216
227
  config.transform.staging_table,
217
- mode="overwrite",
228
+ mode=mode,
218
229
  properties=config.clickhouse.jdbc_properties,
219
230
  )
220
231
  logger.info(
@@ -233,14 +244,15 @@ def stage_to_path(
233
244
  """Write transformed data to parquet/delta (for local dev or intermediate storage)."""
234
245
  path = config.transform.staging_path
235
246
  fmt = config.transform.staging_format
247
+ mode = _normalize_staging_write_mode(config.transform.staging_write_mode)
236
248
  if fmt == "parquet":
237
- df.write.mode("overwrite").parquet(path)
249
+ df.write.mode(mode).parquet(path)
238
250
  logger.info("Staged data to %s (parquet)", path)
239
251
  elif fmt == "delta":
240
- df.write.format("delta").mode("overwrite").save(path)
252
+ df.write.format("delta").mode(mode).save(path)
241
253
  logger.info("Staged data to %s (delta)", path)
242
254
  else:
243
- df.write.format(fmt).mode("overwrite").save(path)
255
+ df.write.format(fmt).mode(mode).save(path)
244
256
  logger.info("Staged data to %s (%s)", path, fmt)
245
257
 
246
258
 
@@ -0,0 +1 @@
1
+ """Utilities shared across batch_analytics (e.g. Gluon / AutoGluon helpers)."""
@@ -0,0 +1,83 @@
1
+ """Shared helpers for AutoGluon train/infer Gluon jobs (S3 + table naming)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Tuple
8
+
9
+
10
+ def parse_s3_uri(uri: str) -> Tuple[str, str]:
11
+ """
12
+ Split ``s3://bucket/key/prefix`` into bucket and key prefix (may be empty).
13
+
14
+ The key prefix does not include a leading slash; trailing slashes are preserved on the key side.
15
+ """
16
+ u = (uri or "").strip()
17
+ if not u.startswith("s3://"):
18
+ raise ValueError(f"Not an s3 URI: {uri!r}")
19
+ rest = u[5:]
20
+ if "/" not in rest:
21
+ return rest, ""
22
+ bucket, key = rest.split("/", 1)
23
+ return bucket, key
24
+
25
+
26
+ def clickhouse_full_table(database: str, table: str) -> str:
27
+ """Build ``db.table`` when ``table`` is unqualified."""
28
+ t = (table or "").strip()
29
+ if not t:
30
+ return t
31
+ if "." in t and "'" not in t:
32
+ return t
33
+ db = (database or "").strip() or "default"
34
+ return f"{db}.{t}"
35
+
36
+
37
+ def local_training_dir() -> str:
38
+ return os.environ.get("AUTOGLUON_LOCAL_MODEL_DIR", "/tmp/autogluon_model")
39
+
40
+
41
+ def boto3_client():
42
+ import boto3
43
+
44
+ return boto3.client("s3")
45
+
46
+
47
+ def upload_directory_to_s3(local_dir: str, s3_dir_uri: str) -> None:
48
+ """Upload every file under ``local_dir`` to ``s3_dir_uri`` (directory URI, trailing ``/`` optional)."""
49
+ bucket, prefix = parse_s3_uri(s3_dir_uri.rstrip("/") + "/")
50
+ prefix = prefix.rstrip("/")
51
+ if prefix:
52
+ prefix = prefix + "/"
53
+ cli = boto3_client()
54
+ root = Path(local_dir)
55
+ for path in root.rglob("*"):
56
+ if not path.is_file():
57
+ continue
58
+ rel = path.relative_to(root).as_posix()
59
+ key = prefix + rel
60
+ cli.upload_file(str(path), bucket, key)
61
+
62
+
63
+ def download_s3_prefix_to_dir(s3_dir_uri: str, local_dir: str) -> None:
64
+ """Download all objects under the S3 prefix implied by ``s3_dir_uri`` into ``local_dir``."""
65
+ bucket, pfx = parse_s3_uri(s3_dir_uri.rstrip("/") + "/")
66
+ pfx = pfx.rstrip("/")
67
+ if pfx:
68
+ pfx = pfx + "/"
69
+ cli = boto3_client()
70
+ os.makedirs(local_dir, exist_ok=True)
71
+ paginator = cli.get_paginator("list_objects_v2")
72
+ pages = paginator.paginate(Bucket=bucket, Prefix=pfx)
73
+ for page in pages:
74
+ for obj in page.get("Contents") or []:
75
+ key = obj["Key"]
76
+ if key.endswith("/"):
77
+ continue
78
+ rel = key[len(pfx) :] if key.startswith(pfx) else key
79
+ if not rel:
80
+ continue
81
+ dest = Path(local_dir) / rel
82
+ dest.parent.mkdir(parents=True, exist_ok=True)
83
+ cli.download_file(bucket, key, str(dest))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batch-analytics
3
- Version: 0.3.13
3
+ Version: 0.3.15
4
4
  Summary: PySpark batch analytics: Extract, Transform, Stage, and analytical modules (linear regression, correlation, PCA, t-test).
5
5
  Author: Litewave Analytics Team
6
6
  License: MIT
@@ -22,11 +22,20 @@ Provides-Extra: output
22
22
  Requires-Dist: boto3>=1.28; extra == "output"
23
23
  Requires-Dist: clickhouse-connect<0.9,>=0.7; python_version < "3.9" and extra == "output"
24
24
  Requires-Dist: clickhouse-connect>=0.7; python_version >= "3.9" and extra == "output"
25
+ Provides-Extra: autogluon
26
+ Requires-Dist: autogluon<2.0,>=1.0; extra == "autogluon"
27
+ Requires-Dist: pandas>=1.3.0; extra == "autogluon"
28
+ Requires-Dist: boto3>=1.28; extra == "autogluon"
29
+ Requires-Dist: clickhouse-connect<0.9,>=0.7; python_version < "3.9" and extra == "autogluon"
30
+ Requires-Dist: clickhouse-connect>=0.7; python_version >= "3.9" and extra == "autogluon"
31
+ Requires-Dist: pyarrow>=10.0.0; python_version >= "3.8" and extra == "autogluon"
25
32
  Provides-Extra: full
26
33
  Requires-Dist: scipy>=1.5.0; extra == "full"
27
34
  Requires-Dist: boto3>=1.28; extra == "full"
28
35
  Requires-Dist: clickhouse-connect<0.9,>=0.7; python_version < "3.9" and extra == "full"
29
36
  Requires-Dist: clickhouse-connect>=0.7; python_version >= "3.9" and extra == "full"
37
+ Requires-Dist: autogluon<2.0,>=1.0; extra == "full"
38
+ Requires-Dist: pyarrow>=10.0.0; python_version >= "3.8" and extra == "full"
30
39
 
31
40
  # Batch Analytics
32
41
 
@@ -16,6 +16,8 @@ src/batch_analytics.egg-info/requires.txt
16
16
  src/batch_analytics.egg-info/top_level.txt
17
17
  src/batch_analytics/analytics/__init__.py
18
18
  src/batch_analytics/analytics/correlation.py
19
+ src/batch_analytics/analytics/gluon_autogluon_infer.py
20
+ src/batch_analytics/analytics/gluon_autogluon_train.py
19
21
  src/batch_analytics/analytics/linear_regression.py
20
22
  src/batch_analytics/analytics/pca_clustering.py
21
23
  src/batch_analytics/analytics/t_test.py
@@ -23,4 +25,6 @@ src/batch_analytics/output/__init__.py
23
25
  src/batch_analytics/output/base.py
24
26
  src/batch_analytics/output/clickhouse.py
25
27
  src/batch_analytics/output/local.py
26
- src/batch_analytics/output/s3.py
28
+ src/batch_analytics/output/s3.py
29
+ src/batch_analytics/utils/__init__.py
30
+ src/batch_analytics/utils/gluon_autogluon_common.py
@@ -2,6 +2,20 @@ pyspark<3.6,>=3.4
2
2
  numpy>=1.19.0
3
3
  scipy>=1.5.0
4
4
 
5
+ [autogluon]
6
+ autogluon<2.0,>=1.0
7
+ pandas>=1.3.0
8
+ boto3>=1.28
9
+
10
+ [autogluon:python_version < "3.9"]
11
+ clickhouse-connect<0.9,>=0.7
12
+
13
+ [autogluon:python_version >= "3.8"]
14
+ pyarrow>=10.0.0
15
+
16
+ [autogluon:python_version >= "3.9"]
17
+ clickhouse-connect>=0.7
18
+
5
19
  [clickhouse]
6
20
 
7
21
  [clickhouse:python_version < "3.9"]
@@ -16,10 +30,14 @@ pytest>=7.0
16
30
  [full]
17
31
  scipy>=1.5.0
18
32
  boto3>=1.28
33
+ autogluon<2.0,>=1.0
19
34
 
20
35
  [full:python_version < "3.9"]
21
36
  clickhouse-connect<0.9,>=0.7
22
37
 
38
+ [full:python_version >= "3.8"]
39
+ pyarrow>=10.0.0
40
+
23
41
  [full:python_version >= "3.9"]
24
42
  clickhouse-connect>=0.7
25
43