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.
Files changed (113) hide show
  1. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/ODPS_Tool.py +64 -22
  2. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +221 -50
  3. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Proc_Compare.py +64 -17
  4. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Weighted_Screen.py +5 -3
  5. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/_common.py +57 -0
  6. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/credit_model.py +15 -0
  7. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/feature_validation.py +26 -8
  8. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/field_meta.py +37 -3
  9. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/reject_inference.py +200 -49
  10. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/sample_analysis.py +17 -2
  11. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/score_comparison.py +101 -13
  12. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/__init__.py +1 -1
  13. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/PKG-INFO +2 -2
  14. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/README.md +1 -1
  15. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  16. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/pyproject.toml +1 -1
  17. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/setup.py +1 -1
  18. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/ExcelFormatTool.py +0 -0
  19. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/ExcelMaster.py +0 -0
  20. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/Template.py +0 -0
  21. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/Utility.py +0 -0
  22. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/ExcelMaster/__init__.py +0 -0
  23. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/LICENSE +0 -0
  24. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/MANIFEST.in +0 -0
  25. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  26. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  27. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  28. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  29. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  30. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  31. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  32. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  33. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  34. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  35. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/__init__.py +0 -0
  36. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/kDataFrame.py +0 -0
  37. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  38. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  39. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Core/utils.py +0 -0
  40. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  41. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  42. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  43. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  44. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/__init__.py +0 -0
  45. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  46. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  47. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  48. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  49. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  50. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Explainability/__init__.py +0 -0
  51. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  52. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  53. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  54. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  55. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
  56. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  57. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  58. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  59. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Feature/__init__.py +0 -0
  60. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  61. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  62. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  63. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  64. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  65. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  66. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  67. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Model/__init__.py +0 -0
  68. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/__init__.py +0 -0
  69. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  70. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  71. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  72. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  73. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  74. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  75. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  76. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  77. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  78. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  79. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/Sample/__init__.py +0 -0
  80. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  81. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/UAT/__init__.py +0 -0
  82. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  83. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  84. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  85. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  86. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  87. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  88. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  89. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  90. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  91. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  92. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  93. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  94. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/__init__.py +0 -0
  95. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  96. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  97. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/__init__.py +0 -0
  98. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/nan_guard.py +0 -0
  99. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/robust.py +0 -0
  100. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/_utils/sentinels.py +0 -0
  101. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  102. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  103. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/__init__.py +0 -0
  104. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  105. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Report/Report_Tool.py +0 -0
  106. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/Report/__init__.py +0 -0
  107. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  108. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  109. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  110. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/requires.txt +0 -0
  111. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  112. {supermodelingfactory-0.5.4 → supermodelingfactory-0.5.6}/requirements.txt +0 -0
  113. {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 i in range(len(df.columns)):
292
- col = df.columns[i]
293
- col_type = "string" if not isnum_dtypes[i] else "float"
294
- col_type = "bigint" if isint_dtypes[i] else col_type
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
- df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
333
- table_schema = self.cre_table_schema(df=df, partition_name=None)
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(df.values.tolist())
380
+ writer.write(records)
343
381
  else:
344
382
  with t.open_writer() as writer:
345
- writer.write(df.values.tolist())
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(df.values.tolist())
401
+ writer.write(records)
364
402
  else:
365
403
  with tmp_table.open_writer() as writer:
366
- writer.write(df.values.tolist())
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.o.run_sql(f"ALTER TABLE {table_name} RENAME TO {old_rename_name};")
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.o.run_sql(f"ALTER TABLE {tmp_name} RENAME TO {target_rename_name};")
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.o.run_sql(f"ALTER TABLE {old_name} RENAME TO {target_rename_name};")
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.o.run_sql(f"ALTER TABLE {table_name} PARTITION ({src}) RENAME TO PARTITION ({dst});")
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(df.values.tolist())
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(df.values.tolist())
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(df.values.tolist())
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
- chunk_id: int,
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
- kwargs = dict(template_kwargs)
70
- kwargs[chunk_filter_key] = f"ABS(HASH({unique_key})) % {n_chunks} = {chunk_id}"
71
- rendered_sql = parse_sql_file(sql_path=sql_path, **kwargs)
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).rstrip().rstrip(";")
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 _resolve_pull_n_chunks(self, count_query: str | None, sql_path: str, template_kwargs: dict[str, Any]) -> int:
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
- count_query = self._auto_count_query(sql_path, template_kwargs)
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 pull(
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
- out_path: str,
155
- count_query: str | None = None,
156
- **template_kwargs: Any,
157
- ) -> dict[str, Any]:
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
- n_chunks = self._resolve_pull_n_chunks(count_query, sql_path, template_kwargs)
160
- cfg.tmp_dir.mkdir(parents=True, exist_ok=True)
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=list(range(n_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)}/{n_chunks} ODPS pull chunks failed:\n{result.errors}")
185
-
186
- chunk_summaries = sorted(result.output, key=lambda item: item["chunk"])
187
- final_path = Path(out_path)
188
- final_path.parent.mkdir(parents=True, exist_ok=True)
189
- final_path.unlink(missing_ok=True)
190
-
191
- with open(final_path, "wb") as fout:
192
- for idx, summary in enumerate(chunk_summaries):
193
- chunk_file = Path(summary["path"])
194
- with open(chunk_file, "rb") as fin:
195
- if idx > 0:
196
- fin.readline()
197
- shutil.copyfileobj(fin, fout)
198
- chunk_file.unlink()
199
-
200
- return {
201
- "n_chunks": n_chunks,
202
- "total_rows": sum(item["rows"] for item in chunk_summaries),
203
- "out_path": str(final_path),
204
- "per_chunk_rows": [item["rows"] for item in chunk_summaries],
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",