SuperModelingFactory 0.5.4__tar.gz → 0.5.6__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.
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/ODPS_Tool.py +64 -22
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +221 -50
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Proc_Compare.py +64 -17
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Weighted_Screen.py +5 -3
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/_common.py +57 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/credit_model.py +15 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/feature_validation.py +26 -8
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/field_meta.py +37 -3
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/reject_inference.py +200 -49
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/sample_analysis.py +17 -2
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/score_comparison.py +101 -13
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/__init__.py +1 -1
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/PKG-INFO +2 -2
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/README.md +1 -1
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/pyproject.toml +1 -1
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/setup.py +1 -1
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/ExcelFormatTool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/ExcelMaster.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/Template.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/Utility.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/LICENSE +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/MANIFEST.in +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Binning_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Slope_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/kDataFrame.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/utils.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/evaluate_model.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/Backward_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/LRM_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Sample_Split.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/UAT/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Master.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/nan_guard.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/robust.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/sentinels.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/simsun.ttc +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Report/Report_Tool.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Report/__init__.py +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/requires.txt +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/top_level.txt +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/requirements.txt +0 -0
- {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/setup.cfg +0 -0
|
@@ -281,17 +281,31 @@ class ODPSRunner(object):
|
|
|
281
281
|
|
|
282
282
|
@staticmethod
|
|
283
283
|
def cre_table_schema(df, partition_name=None):
|
|
284
|
-
|
|
285
|
-
dtypes = df.dtypes
|
|
286
|
-
isnum_dtypes = [pd.api.types.is_numeric_dtype(x) for x in dtypes]
|
|
287
|
-
isint_dtypes = [pd.api.types.is_integer_dtype(x) for x in dtypes]
|
|
288
|
-
|
|
289
284
|
table_columns = []
|
|
290
285
|
table_partitions = []
|
|
291
|
-
for
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
286
|
+
for col, dtype in df.dtypes.items():
|
|
287
|
+
if pd.api.types.is_complex_dtype(dtype) or pd.api.types.is_timedelta64_dtype(dtype):
|
|
288
|
+
raise TypeError(
|
|
289
|
+
f"Unsupported dtype for ODPS schema inference: column={col!r}, dtype={dtype!s}."
|
|
290
|
+
)
|
|
291
|
+
if pd.api.types.is_bool_dtype(dtype):
|
|
292
|
+
col_type = "boolean"
|
|
293
|
+
elif pd.api.types.is_integer_dtype(dtype):
|
|
294
|
+
col_type = "bigint"
|
|
295
|
+
elif pd.api.types.is_float_dtype(dtype):
|
|
296
|
+
col_type = "float" if str(dtype).lower() == "float32" else "double"
|
|
297
|
+
elif pd.api.types.is_datetime64_any_dtype(dtype):
|
|
298
|
+
col_type = "datetime"
|
|
299
|
+
elif (
|
|
300
|
+
pd.api.types.is_string_dtype(dtype)
|
|
301
|
+
or pd.api.types.is_object_dtype(dtype)
|
|
302
|
+
or isinstance(dtype, pd.CategoricalDtype)
|
|
303
|
+
):
|
|
304
|
+
col_type = "string"
|
|
305
|
+
else:
|
|
306
|
+
raise TypeError(
|
|
307
|
+
f"Unsupported dtype for ODPS schema inference: column={col!r}, dtype={dtype!s}."
|
|
308
|
+
)
|
|
295
309
|
if col == partition_name:
|
|
296
310
|
table_partitions.append(Partition(name=col, type=col_type))
|
|
297
311
|
else:
|
|
@@ -304,6 +318,28 @@ class ODPSRunner(object):
|
|
|
304
318
|
|
|
305
319
|
return table_schema
|
|
306
320
|
|
|
321
|
+
@staticmethod
|
|
322
|
+
def _to_odps_records(df):
|
|
323
|
+
"""Return Python records with pandas missing values converted to ODPS NULL."""
|
|
324
|
+
object_df = df.astype(object)
|
|
325
|
+
object_df = object_df.where(pd.notna(object_df), None)
|
|
326
|
+
return object_df.values.tolist()
|
|
327
|
+
|
|
328
|
+
def _execute_sql_blocking(self, sql):
|
|
329
|
+
"""Execute DDL and wait until the remote ODPS instance has completed."""
|
|
330
|
+
execute_sql = getattr(self.o, "execute_sql", None)
|
|
331
|
+
if callable(execute_sql):
|
|
332
|
+
return execute_sql(sql)
|
|
333
|
+
|
|
334
|
+
run_sql = getattr(self.o, "run_sql", None)
|
|
335
|
+
if not callable(run_sql):
|
|
336
|
+
raise AttributeError("ODPS client must provide execute_sql() or run_sql().")
|
|
337
|
+
instance = run_sql(sql)
|
|
338
|
+
wait_for_success = getattr(instance, "wait_for_success", None)
|
|
339
|
+
if callable(wait_for_success):
|
|
340
|
+
wait_for_success()
|
|
341
|
+
return instance
|
|
342
|
+
|
|
307
343
|
def upload_df(self, df, table_name, table_schema=None, partition=None, atomic=True):
|
|
308
344
|
"""上传数据集至mc中创建新表
|
|
309
345
|
|
|
@@ -328,9 +364,11 @@ class ODPSRunner(object):
|
|
|
328
364
|
legacy behaviour, e.g. for tables whose downstream readers expect
|
|
329
365
|
a specific object identity.
|
|
330
366
|
"""
|
|
367
|
+
upload_data = df.copy()
|
|
331
368
|
if table_schema is None:
|
|
332
|
-
|
|
333
|
-
table_schema = self.cre_table_schema(df=
|
|
369
|
+
upload_data.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
370
|
+
table_schema = self.cre_table_schema(df=upload_data, partition_name=None)
|
|
371
|
+
records = self._to_odps_records(upload_data)
|
|
334
372
|
|
|
335
373
|
if not atomic:
|
|
336
374
|
# Legacy pre-0.4.2 non-atomic path — retained under an explicit
|
|
@@ -339,10 +377,10 @@ class ODPSRunner(object):
|
|
|
339
377
|
t = self.o.create_table(table_name, table_schema)
|
|
340
378
|
if bool(partition):
|
|
341
379
|
with t.open_writer(partition=partition, create_partition=True) as writer:
|
|
342
|
-
writer.write(
|
|
380
|
+
writer.write(records)
|
|
343
381
|
else:
|
|
344
382
|
with t.open_writer() as writer:
|
|
345
|
-
writer.write(
|
|
383
|
+
writer.write(records)
|
|
346
384
|
logger.info(f'<<<< 完成数据入表{table_name}: shape={df.shape} >>>>')
|
|
347
385
|
return
|
|
348
386
|
|
|
@@ -360,10 +398,10 @@ class ODPSRunner(object):
|
|
|
360
398
|
try:
|
|
361
399
|
if bool(partition):
|
|
362
400
|
with tmp_table.open_writer(partition=partition, create_partition=True) as writer:
|
|
363
|
-
writer.write(
|
|
401
|
+
writer.write(records)
|
|
364
402
|
else:
|
|
365
403
|
with tmp_table.open_writer() as writer:
|
|
366
|
-
writer.write(
|
|
404
|
+
writer.write(records)
|
|
367
405
|
except Exception:
|
|
368
406
|
try:
|
|
369
407
|
self.o.delete_table(tmp_name, if_exists=True)
|
|
@@ -379,7 +417,7 @@ class ODPSRunner(object):
|
|
|
379
417
|
target_exists = self.o.exist_table(table_name)
|
|
380
418
|
if target_exists:
|
|
381
419
|
try:
|
|
382
|
-
self.
|
|
420
|
+
self._execute_sql_blocking(f"ALTER TABLE {table_name} RENAME TO {old_rename_name};")
|
|
383
421
|
except Exception:
|
|
384
422
|
# Rename of the live target failed — do not touch it. Drop the
|
|
385
423
|
# tmp table so we don't leak it, then re-raise.
|
|
@@ -392,13 +430,13 @@ class ODPSRunner(object):
|
|
|
392
430
|
raise
|
|
393
431
|
|
|
394
432
|
try:
|
|
395
|
-
self.
|
|
433
|
+
self._execute_sql_blocking(f"ALTER TABLE {tmp_name} RENAME TO {target_rename_name};")
|
|
396
434
|
except Exception:
|
|
397
435
|
# tmp -> target rename failed. If we already moved the original,
|
|
398
436
|
# try to restore it so the caller is not left with a missing table.
|
|
399
437
|
if target_exists:
|
|
400
438
|
try:
|
|
401
|
-
self.
|
|
439
|
+
self._execute_sql_blocking(f"ALTER TABLE {old_name} RENAME TO {target_rename_name};")
|
|
402
440
|
logger.warning(
|
|
403
441
|
f"upload_df: swap of {tmp_name} -> {table_name} failed; restored original from {old_name}"
|
|
404
442
|
)
|
|
@@ -439,7 +477,9 @@ class ODPSRunner(object):
|
|
|
439
477
|
def _rename_partition(self, table_name, source_partition, target_partition):
|
|
440
478
|
src = _format_odps_partition_spec(_parse_odps_partition_spec(source_partition), quoted=True)
|
|
441
479
|
dst = _format_odps_partition_spec(_parse_odps_partition_spec(target_partition), quoted=True)
|
|
442
|
-
self.
|
|
480
|
+
self._execute_sql_blocking(
|
|
481
|
+
f"ALTER TABLE {table_name} PARTITION ({src}) RENAME TO PARTITION ({dst});"
|
|
482
|
+
)
|
|
443
483
|
|
|
444
484
|
def insert_df(self, df, table_name, overwrite=True, partition=None, atomic=True):
|
|
445
485
|
"""将数据集插入至mc已存在的表中.
|
|
@@ -457,7 +497,9 @@ class ODPSRunner(object):
|
|
|
457
497
|
"""
|
|
458
498
|
t = self.o.get_table(table_name)
|
|
459
499
|
if "py_inserttime" in t.schema and "py_inserttime" not in df.columns:
|
|
500
|
+
df = df.copy()
|
|
460
501
|
df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
502
|
+
records = self._to_odps_records(df)
|
|
461
503
|
|
|
462
504
|
if bool(partition):
|
|
463
505
|
if overwrite and atomic:
|
|
@@ -476,7 +518,7 @@ class ODPSRunner(object):
|
|
|
476
518
|
|
|
477
519
|
try:
|
|
478
520
|
with t.open_writer(partition=staging_partition, create_partition=True) as writer:
|
|
479
|
-
writer.write(
|
|
521
|
+
writer.write(records)
|
|
480
522
|
if target_exists:
|
|
481
523
|
self._rename_partition(table_name, partition, backup_partition)
|
|
482
524
|
self._rename_partition(table_name, staging_partition, partition)
|
|
@@ -510,10 +552,10 @@ class ODPSRunner(object):
|
|
|
510
552
|
if overwrite:
|
|
511
553
|
t.delete_partition(partition, if_exists=True)
|
|
512
554
|
with t.open_writer(partition=partition, create_partition=True) as writer:
|
|
513
|
-
writer.write(
|
|
555
|
+
writer.write(records)
|
|
514
556
|
else:
|
|
515
557
|
if overwrite:
|
|
516
558
|
t.truncate()
|
|
517
559
|
with t.open_writer() as writer:
|
|
518
|
-
writer.write(
|
|
560
|
+
writer.write(records)
|
|
519
561
|
logger.info('<<<< 完成数据入表: shape={0} >>>>'.format(df.shape))
|
|
@@ -17,6 +17,7 @@ from .utils import parse_sql_file
|
|
|
17
17
|
|
|
18
18
|
Backend = Literal["thread", "process", "sequential"]
|
|
19
19
|
WriteMode = Literal["overwrite", "append"]
|
|
20
|
+
PullSplitStrategy = Literal["auto", "hash", "row_number"]
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
@dataclass
|
|
@@ -26,6 +27,10 @@ class ParallelODPSConfig:
|
|
|
26
27
|
n_chunks: int | None = None
|
|
27
28
|
n_jobs: int = 3
|
|
28
29
|
backend: Backend = "thread"
|
|
30
|
+
pull_split_strategy: PullSplitStrategy = "auto"
|
|
31
|
+
row_number_order_by: str | None = None
|
|
32
|
+
row_number_col: str = "__smf_parallel_odps_rn__"
|
|
33
|
+
validate_unique_key: bool = True
|
|
29
34
|
chunk_filter_key: str = "chunk_filter"
|
|
30
35
|
tmp_dir: Path = field(default_factory=lambda: Path("data/_chunks"))
|
|
31
36
|
tmp_table_prefix: str = "tmp_parallel_odps"
|
|
@@ -55,22 +60,23 @@ def _delete_table(runner: ODPSRunner, table_name: str) -> None:
|
|
|
55
60
|
runner.run_sql(f"DROP TABLE IF EXISTS {table_name};", to_df=False)
|
|
56
61
|
|
|
57
62
|
|
|
63
|
+
def _strip_trailing_semicolon(sql: str) -> str:
|
|
64
|
+
return sql.strip().rstrip(";").strip()
|
|
65
|
+
|
|
66
|
+
|
|
58
67
|
def _pull_one_chunk(
|
|
59
|
-
|
|
68
|
+
chunk_spec: dict[str, Any],
|
|
60
69
|
runner: ODPSRunner | None,
|
|
61
|
-
sql_path: str,
|
|
62
|
-
template_kwargs: dict[str, Any],
|
|
63
|
-
chunk_filter_key: str,
|
|
64
|
-
unique_key: str,
|
|
65
|
-
n_chunks: int,
|
|
66
70
|
tmp_dir_str: str,
|
|
67
71
|
) -> dict[str, Any]:
|
|
68
72
|
active_runner = _get_runner(runner)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
chunk_id = int(chunk_spec["chunk"])
|
|
74
|
+
rendered_sql = str(chunk_spec["sql"])
|
|
75
|
+
drop_cols = list(chunk_spec.get("drop_cols") or [])
|
|
72
76
|
|
|
73
77
|
df_chunk = active_runner.run_sql(rendered_sql, to_df=True, n_process=1)
|
|
78
|
+
if drop_cols:
|
|
79
|
+
df_chunk = df_chunk.drop(columns=[col for col in drop_cols if col in df_chunk.columns])
|
|
74
80
|
chunk_path = Path(tmp_dir_str) / f"_pull_chunk_{chunk_id:04d}.csv"
|
|
75
81
|
df_chunk.to_csv(chunk_path, index=False)
|
|
76
82
|
n_rows = len(df_chunk)
|
|
@@ -102,6 +108,7 @@ class ParallelODPSManager:
|
|
|
102
108
|
|
|
103
109
|
_VALID_BACKENDS = {"thread", "process", "sequential"}
|
|
104
110
|
_VALID_WRITE_MODES = {"overwrite", "append"}
|
|
111
|
+
_VALID_PULL_STRATEGIES = {"auto", "hash", "row_number"}
|
|
105
112
|
|
|
106
113
|
def __init__(self, config: ParallelODPSConfig, odps_runner: ODPSRunner | None = None):
|
|
107
114
|
self.config = config
|
|
@@ -112,6 +119,8 @@ class ParallelODPSManager:
|
|
|
112
119
|
cfg = self.config
|
|
113
120
|
if cfg.backend not in self._VALID_BACKENDS:
|
|
114
121
|
raise ValueError(f"backend must be one of {sorted(self._VALID_BACKENDS)}")
|
|
122
|
+
if cfg.pull_split_strategy not in self._VALID_PULL_STRATEGIES:
|
|
123
|
+
raise ValueError(f"pull_split_strategy must be one of {sorted(self._VALID_PULL_STRATEGIES)}")
|
|
115
124
|
if cfg.chunk_size is not None and cfg.chunk_size <= 0:
|
|
116
125
|
raise ValueError("chunk_size must be a positive integer.")
|
|
117
126
|
if cfg.n_chunks is not None and cfg.n_chunks <= 0:
|
|
@@ -122,6 +131,8 @@ class ParallelODPSManager:
|
|
|
122
131
|
raise ValueError("n_jobs must be a positive integer.")
|
|
123
132
|
if not cfg.chunk_filter_key:
|
|
124
133
|
raise ValueError("chunk_filter_key cannot be empty.")
|
|
134
|
+
if not cfg.row_number_col:
|
|
135
|
+
raise ValueError("row_number_col cannot be empty.")
|
|
125
136
|
if not cfg.tmp_table_prefix:
|
|
126
137
|
raise ValueError("tmp_table_prefix cannot be empty.")
|
|
127
138
|
|
|
@@ -131,34 +142,151 @@ class ParallelODPSManager:
|
|
|
131
142
|
def _auto_count_query(self, sql_path: str, template_kwargs: dict[str, Any]) -> str:
|
|
132
143
|
kwargs = dict(template_kwargs)
|
|
133
144
|
kwargs[self.config.chunk_filter_key] = "1=1"
|
|
134
|
-
rendered_sql = parse_sql_file(sql_path=sql_path, **kwargs)
|
|
145
|
+
rendered_sql = _strip_trailing_semicolon(parse_sql_file(sql_path=sql_path, **kwargs))
|
|
135
146
|
return f"SELECT COUNT(1) FROM ({rendered_sql}) __count_src;"
|
|
136
147
|
|
|
137
|
-
def
|
|
148
|
+
def _assert_chunk_filter_placeholder(self, sql_path: str, template_kwargs: dict[str, Any]) -> None:
|
|
149
|
+
sentinel = "__SMF_CHUNK_FILTER_SENTINEL__=1"
|
|
150
|
+
kwargs = dict(template_kwargs)
|
|
151
|
+
kwargs[self.config.chunk_filter_key] = sentinel
|
|
152
|
+
rendered_sql = parse_sql_file(sql_path=sql_path, **kwargs)
|
|
153
|
+
if sentinel not in rendered_sql:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"pull SQL template must contain {{{self.config.chunk_filter_key}}}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _render_pull_sql(self, sql_path: str, template_kwargs: dict[str, Any], chunk_filter: str) -> str:
|
|
159
|
+
kwargs = dict(template_kwargs)
|
|
160
|
+
kwargs[self.config.chunk_filter_key] = chunk_filter
|
|
161
|
+
return parse_sql_file(sql_path=sql_path, **kwargs)
|
|
162
|
+
|
|
163
|
+
def _resolve_pull_strategy(self) -> str:
|
|
164
|
+
cfg = self.config
|
|
165
|
+
strategy = cfg.pull_split_strategy
|
|
166
|
+
if strategy == "auto":
|
|
167
|
+
return "hash" if cfg.unique_key else "row_number"
|
|
168
|
+
if strategy == "hash" and not cfg.unique_key:
|
|
169
|
+
raise ValueError("unique_key is required when pull_split_strategy='hash'.")
|
|
170
|
+
return strategy
|
|
171
|
+
|
|
172
|
+
def _resolve_pull_n_chunks(
|
|
173
|
+
self,
|
|
174
|
+
count_query: str | None,
|
|
175
|
+
sql_path: str,
|
|
176
|
+
template_kwargs: dict[str, Any],
|
|
177
|
+
strategy: str,
|
|
178
|
+
staging_table: str | None = None,
|
|
179
|
+
) -> int:
|
|
138
180
|
cfg = self.config
|
|
139
|
-
if not cfg.unique_key:
|
|
140
|
-
raise ValueError("unique_key is required for pull().")
|
|
141
181
|
if cfg.n_chunks is not None:
|
|
142
182
|
return cfg.n_chunks
|
|
143
183
|
if cfg.chunk_size is None:
|
|
144
184
|
raise ValueError("chunk_size or n_chunks is required for pull().")
|
|
145
185
|
if count_query is None:
|
|
146
|
-
|
|
186
|
+
if strategy == "row_number":
|
|
187
|
+
if not staging_table:
|
|
188
|
+
raise ValueError("staging_table is required to count row_number pull chunks.")
|
|
189
|
+
count_query = f"SELECT COUNT(1) FROM {staging_table};"
|
|
190
|
+
else:
|
|
191
|
+
count_query = self._auto_count_query(sql_path, template_kwargs)
|
|
147
192
|
count_df = self.odps_runner.run_sql(count_query, to_df=True)
|
|
148
193
|
total_rows = int(count_df.iloc[0, 0])
|
|
149
194
|
return max(1, math.ceil(total_rows / cfg.chunk_size))
|
|
150
195
|
|
|
151
|
-
def
|
|
196
|
+
def _hash_chunk_filter(self, chunk_id: int, n_chunks: int) -> str:
|
|
197
|
+
return f"ABS(HASH({self.config.unique_key})) % {n_chunks} = {chunk_id}"
|
|
198
|
+
|
|
199
|
+
def _validate_hash_pull_sql(
|
|
152
200
|
self,
|
|
153
201
|
sql_path: str,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
202
|
+
template_kwargs: dict[str, Any],
|
|
203
|
+
n_chunks: int,
|
|
204
|
+
) -> None:
|
|
205
|
+
if not self.config.validate_unique_key:
|
|
206
|
+
return
|
|
207
|
+
rendered_sql = self._render_pull_sql(
|
|
208
|
+
sql_path=sql_path,
|
|
209
|
+
template_kwargs=template_kwargs,
|
|
210
|
+
chunk_filter=self._hash_chunk_filter(0, n_chunks),
|
|
211
|
+
)
|
|
212
|
+
probe_sql = (
|
|
213
|
+
"SELECT * FROM (\n"
|
|
214
|
+
f"{_strip_trailing_semicolon(rendered_sql)}\n"
|
|
215
|
+
") __smf_unique_key_probe LIMIT 1;"
|
|
216
|
+
)
|
|
217
|
+
try:
|
|
218
|
+
self.odps_runner.run_sql(probe_sql, to_df=True, n_process=1)
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"unique_key validation failed for pull SQL. "
|
|
222
|
+
f"Check that unique_key={self.config.unique_key!r} is visible in the SQL scope."
|
|
223
|
+
) from exc
|
|
224
|
+
|
|
225
|
+
def _build_hash_pull_chunks(
|
|
226
|
+
self,
|
|
227
|
+
sql_path: str,
|
|
228
|
+
template_kwargs: dict[str, Any],
|
|
229
|
+
n_chunks: int,
|
|
230
|
+
) -> list[dict[str, Any]]:
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
"chunk": chunk_id,
|
|
234
|
+
"sql": self._render_pull_sql(
|
|
235
|
+
sql_path=sql_path,
|
|
236
|
+
template_kwargs=template_kwargs,
|
|
237
|
+
chunk_filter=self._hash_chunk_filter(chunk_id, n_chunks),
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
for chunk_id in range(n_chunks)
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
def _row_number_staging_table_name(self, run_id: str) -> str:
|
|
244
|
+
return f"{self.config.tmp_table_prefix}_{run_id}_pull_stage"
|
|
245
|
+
|
|
246
|
+
def _create_row_number_staging_table(
|
|
247
|
+
self,
|
|
248
|
+
sql_path: str,
|
|
249
|
+
template_kwargs: dict[str, Any],
|
|
250
|
+
staging_table: str,
|
|
251
|
+
) -> str:
|
|
158
252
|
cfg = self.config
|
|
159
|
-
|
|
160
|
-
|
|
253
|
+
base_sql = _strip_trailing_semicolon(
|
|
254
|
+
self._render_pull_sql(
|
|
255
|
+
sql_path=sql_path,
|
|
256
|
+
template_kwargs=template_kwargs,
|
|
257
|
+
chunk_filter="1=1",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
order_by = cfg.row_number_order_by or "1"
|
|
261
|
+
create_sql = (
|
|
262
|
+
f"CREATE TABLE {staging_table} AS\n"
|
|
263
|
+
"SELECT\n"
|
|
264
|
+
f" ROW_NUMBER() OVER (ORDER BY {order_by}) AS {cfg.row_number_col},\n"
|
|
265
|
+
" __smf_base.*\n"
|
|
266
|
+
"FROM (\n"
|
|
267
|
+
f"{base_sql}\n"
|
|
268
|
+
") __smf_base;"
|
|
269
|
+
)
|
|
270
|
+
self.odps_runner.run_sql(create_sql, to_df=False)
|
|
271
|
+
return create_sql
|
|
272
|
+
|
|
273
|
+
def _build_row_number_pull_chunks(self, staging_table: str, n_chunks: int) -> list[dict[str, Any]]:
|
|
274
|
+
row_number_col = self.config.row_number_col
|
|
275
|
+
return [
|
|
276
|
+
{
|
|
277
|
+
"chunk": chunk_id,
|
|
278
|
+
"sql": (
|
|
279
|
+
f"SELECT *\n"
|
|
280
|
+
f"FROM {staging_table}\n"
|
|
281
|
+
f"WHERE ({row_number_col} - 1) % {n_chunks} = {chunk_id};"
|
|
282
|
+
),
|
|
283
|
+
"drop_cols": [row_number_col],
|
|
284
|
+
}
|
|
285
|
+
for chunk_id in range(n_chunks)
|
|
286
|
+
]
|
|
161
287
|
|
|
288
|
+
def _run_pull_chunks(self, chunk_specs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
289
|
+
cfg = self.config
|
|
162
290
|
engine_cfg = ParallelApplyConfig(
|
|
163
291
|
split_axis="chunk",
|
|
164
292
|
backend=cfg.backend,
|
|
@@ -168,41 +296,83 @@ class ParallelODPSManager:
|
|
|
168
296
|
)
|
|
169
297
|
result = ParallelApplyEngine(engine_cfg).run(
|
|
170
298
|
func=_pull_one_chunk,
|
|
171
|
-
chunks=
|
|
299
|
+
chunks=chunk_specs,
|
|
172
300
|
func_args=(
|
|
173
301
|
self._runner_for_backend(),
|
|
174
|
-
sql_path,
|
|
175
|
-
template_kwargs,
|
|
176
|
-
cfg.chunk_filter_key,
|
|
177
|
-
cfg.unique_key,
|
|
178
|
-
n_chunks,
|
|
179
302
|
str(cfg.tmp_dir),
|
|
180
303
|
),
|
|
181
304
|
)
|
|
182
|
-
|
|
183
305
|
if len(result.errors):
|
|
184
|
-
raise RuntimeError(f"{len(result.errors)}/{
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
306
|
+
raise RuntimeError(f"{len(result.errors)}/{len(chunk_specs)} ODPS pull chunks failed:\n{result.errors}")
|
|
307
|
+
return sorted(result.output, key=lambda item: item["chunk"])
|
|
308
|
+
|
|
309
|
+
def pull(
|
|
310
|
+
self,
|
|
311
|
+
sql_path: str,
|
|
312
|
+
out_path: str,
|
|
313
|
+
count_query: str | None = None,
|
|
314
|
+
**template_kwargs: Any,
|
|
315
|
+
) -> dict[str, Any]:
|
|
316
|
+
cfg = self.config
|
|
317
|
+
self._assert_chunk_filter_placeholder(sql_path, template_kwargs)
|
|
318
|
+
cfg.tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
strategy = self._resolve_pull_strategy()
|
|
320
|
+
staging_table: str | None = None
|
|
321
|
+
success = False
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
if strategy == "row_number":
|
|
325
|
+
run_id = uuid.uuid4().hex[:12]
|
|
326
|
+
staging_table = self._row_number_staging_table_name(run_id)
|
|
327
|
+
self._create_row_number_staging_table(sql_path, template_kwargs, staging_table)
|
|
328
|
+
n_chunks = self._resolve_pull_n_chunks(
|
|
329
|
+
count_query=count_query,
|
|
330
|
+
sql_path=sql_path,
|
|
331
|
+
template_kwargs=template_kwargs,
|
|
332
|
+
strategy=strategy,
|
|
333
|
+
staging_table=staging_table,
|
|
334
|
+
)
|
|
335
|
+
chunk_specs = self._build_row_number_pull_chunks(staging_table, n_chunks)
|
|
336
|
+
else:
|
|
337
|
+
n_chunks = self._resolve_pull_n_chunks(
|
|
338
|
+
count_query=count_query,
|
|
339
|
+
sql_path=sql_path,
|
|
340
|
+
template_kwargs=template_kwargs,
|
|
341
|
+
strategy=strategy,
|
|
342
|
+
)
|
|
343
|
+
self._validate_hash_pull_sql(sql_path, template_kwargs, n_chunks)
|
|
344
|
+
chunk_specs = self._build_hash_pull_chunks(sql_path, template_kwargs, n_chunks)
|
|
345
|
+
|
|
346
|
+
chunk_summaries = self._run_pull_chunks(chunk_specs)
|
|
347
|
+
final_path = Path(out_path)
|
|
348
|
+
final_path.parent.mkdir(parents=True, exist_ok=True)
|
|
349
|
+
final_path.unlink(missing_ok=True)
|
|
350
|
+
|
|
351
|
+
with open(final_path, "wb") as fout:
|
|
352
|
+
for idx, summary in enumerate(chunk_summaries):
|
|
353
|
+
chunk_file = Path(summary["path"])
|
|
354
|
+
with open(chunk_file, "rb") as fin:
|
|
355
|
+
if idx > 0:
|
|
356
|
+
fin.readline()
|
|
357
|
+
shutil.copyfileobj(fin, fout)
|
|
358
|
+
chunk_file.unlink()
|
|
359
|
+
|
|
360
|
+
success = True
|
|
361
|
+
return {
|
|
362
|
+
"pull_strategy": strategy,
|
|
363
|
+
"staging_table": staging_table,
|
|
364
|
+
"n_chunks": n_chunks,
|
|
365
|
+
"total_rows": sum(item["rows"] for item in chunk_summaries),
|
|
366
|
+
"out_path": str(final_path),
|
|
367
|
+
"per_chunk_rows": [item["rows"] for item in chunk_summaries],
|
|
368
|
+
}
|
|
369
|
+
finally:
|
|
370
|
+
if (
|
|
371
|
+
staging_table
|
|
372
|
+
and cfg.cleanup_tmp
|
|
373
|
+
and (success or not cfg.keep_tmp_on_error)
|
|
374
|
+
):
|
|
375
|
+
_delete_table(self.odps_runner, staging_table)
|
|
206
376
|
|
|
207
377
|
def push(
|
|
208
378
|
self,
|
|
@@ -223,6 +393,7 @@ class ParallelODPSManager:
|
|
|
223
393
|
raise ValueError("push data produced no chunks.")
|
|
224
394
|
|
|
225
395
|
tmp_tables = [spec["tmp_table"] for spec in chunk_specs]
|
|
396
|
+
success = False
|
|
226
397
|
try:
|
|
227
398
|
engine_cfg = ParallelApplyConfig(
|
|
228
399
|
split_axis="chunk",
|