PyIAML 1.0.0__py3-none-any.whl
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.
- iaml/__init__.py +56 -0
- iaml/actionable.py +11 -0
- iaml/actionables/__init__.py +21 -0
- iaml/actionables/boosting/__init__.py +4 -0
- iaml/actionables/boosting/act_adaboost.py +59 -0
- iaml/actionables/cleaning/__init__.py +26 -0
- iaml/actionables/cleaning/act_categorical_imputer.py +124 -0
- iaml/actionables/cleaning/act_count_vectorizer.py +204 -0
- iaml/actionables/cleaning/act_drop_categorical_column.py +51 -0
- iaml/actionables/cleaning/act_drop_date_column.py +48 -0
- iaml/actionables/cleaning/act_drop_high_cardinality_categorical.py +337 -0
- iaml/actionables/cleaning/act_drop_numerical_column.py +75 -0
- iaml/actionables/cleaning/act_drop_textual_column.py +51 -0
- iaml/actionables/cleaning/act_encode_target_column.py +56 -0
- iaml/actionables/cleaning/act_frequency_encoder.py +127 -0
- iaml/actionables/cleaning/act_hashing_vectorizer.py +186 -0
- iaml/actionables/cleaning/act_knn_imputer.py +152 -0
- iaml/actionables/cleaning/act_mean_column.py +79 -0
- iaml/actionables/cleaning/act_mice.py +464 -0
- iaml/actionables/cleaning/act_missing_count_feature.py +109 -0
- iaml/actionables/cleaning/act_missing_indicator.py +124 -0
- iaml/actionables/cleaning/act_onehot.py +65 -0
- iaml/actionables/cleaning/act_ordinal_encoder.py +177 -0
- iaml/actionables/cleaning/act_rare_category_grouper.py +173 -0
- iaml/actionables/cleaning/act_simple_imputer.py +109 -0
- iaml/actionables/cleaning/act_split_date.py +68 -0
- iaml/actionables/cleaning/act_target_encoder.py +274 -0
- iaml/actionables/cleaning/act_text_normalizer.py +241 -0
- iaml/actionables/cleaning/act_tf_idf.py +80 -0
- iaml/actionables/cleaning/act_word2vec.py +150 -0
- iaml/actionables/features_precleaning/__init__.py +12 -0
- iaml/actionables/features_precleaning/act_coerce_numeric_strings.py +194 -0
- iaml/actionables/features_precleaning/act_date_converter.py +99 -0
- iaml/actionables/features_precleaning/act_drop_bad_quality_rows.py +77 -0
- iaml/actionables/features_precleaning/act_drop_duplicate_rows.py +131 -0
- iaml/actionables/features_precleaning/act_drop_high_missing_columns.py +94 -0
- iaml/actionables/features_precleaning/act_drop_id_like_columns.py +294 -0
- iaml/actionables/features_precleaning/act_normalize_column_names.py +157 -0
- iaml/actionables/features_precleaning/act_sentinel_to_na_n.py +270 -0
- iaml/actionables/features_precleaning/act_trim_space.py +79 -0
- iaml/actionables/features_preprocessing/__init__.py +18 -0
- iaml/actionables/features_preprocessing/act_cyclical_date_encoding.py +212 -0
- iaml/actionables/features_preprocessing/act_fast_ica.py +161 -0
- iaml/actionables/features_preprocessing/act_feature_agglomeration.py +90 -0
- iaml/actionables/features_preprocessing/act_k_bins_discretizer.py +207 -0
- iaml/actionables/features_preprocessing/act_k_means_features.py +296 -0
- iaml/actionables/features_preprocessing/act_kernel_pca.py +143 -0
- iaml/actionables/features_preprocessing/act_log_transformer.py +122 -0
- iaml/actionables/features_preprocessing/act_nystroem.py +100 -0
- iaml/actionables/features_preprocessing/act_pca.py +77 -0
- iaml/actionables/features_preprocessing/act_polynomial_features.py +86 -0
- iaml/actionables/features_preprocessing/act_power_transformer.py +106 -0
- iaml/actionables/features_preprocessing/act_quantile_transformer.py +114 -0
- iaml/actionables/features_preprocessing/act_rbf_sampler.py +88 -0
- iaml/actionables/features_preprocessing/act_select_percentile.py +112 -0
- iaml/actionables/features_preprocessing/act_sparse_random_projection.py +157 -0
- iaml/actionables/features_preprocessing/act_truncated_svd.py +137 -0
- iaml/actionables/features_selection/__init__.py +8 -0
- iaml/actionables/features_selection/act_permutation_importance_selector.py +421 -0
- iaml/actionables/features_selection/act_remove_high_correlated_column.py +70 -0
- iaml/actionables/features_selection/act_remove_low_variance_column.py +74 -0
- iaml/actionables/features_selection/act_rfe.py +214 -0
- iaml/actionables/features_selection/act_select_from_model.py +325 -0
- iaml/actionables/features_selection/act_select_k_best.py +181 -0
- iaml/actionables/features_selection/act_vif_selector.py +130 -0
- iaml/actionables/imbalance/__init__.py +10 -0
- iaml/actionables/imbalance/act_adasyn.py +150 -0
- iaml/actionables/imbalance/act_borderline_smote.py +171 -0
- iaml/actionables/imbalance/act_near_miss.py +158 -0
- iaml/actionables/imbalance/act_random_over_sampling.py +60 -0
- iaml/actionables/imbalance/act_random_under_sampler.py +135 -0
- iaml/actionables/imbalance/act_smote.py +162 -0
- iaml/actionables/imbalance/act_smote_tomek.py +182 -0
- iaml/actionables/imbalance/act_smoteenn.py +193 -0
- iaml/actionables/imbalance/act_tomek_links.py +138 -0
- iaml/actionables/normalize/__init__.py +6 -0
- iaml/actionables/normalize/act_max_abs_scaler.py +78 -0
- iaml/actionables/normalize/act_minmax_scaler.py +56 -0
- iaml/actionables/normalize/act_normalizer.py +95 -0
- iaml/actionables/normalize/act_robust_scaler.py +111 -0
- iaml/actionables/normalize/act_standard_scaler.py +55 -0
- iaml/actionables/predictors/__init__.py +6 -0
- iaml/actionables/predictors/_xgboost.py +16 -0
- iaml/actionables/predictors/classifier/__init__.py +26 -0
- iaml/actionables/predictors/classifier/act_bagging_classifier.py +113 -0
- iaml/actionables/predictors/classifier/act_bernoulli_nb.py +89 -0
- iaml/actionables/predictors/classifier/act_catboost_classifier.py +135 -0
- iaml/actionables/predictors/classifier/act_complement_nb.py +106 -0
- iaml/actionables/predictors/classifier/act_decision_tree_classifier.py +117 -0
- iaml/actionables/predictors/classifier/act_extra_trees_classifier.py +115 -0
- iaml/actionables/predictors/classifier/act_gaussian_nb.py +53 -0
- iaml/actionables/predictors/classifier/act_hist_gradient_boosting_classifier.py +144 -0
- iaml/actionables/predictors/classifier/act_knn.py +86 -0
- iaml/actionables/predictors/classifier/act_light_gbm_classifier.py +211 -0
- iaml/actionables/predictors/classifier/act_linear_discriminant_analysis.py +63 -0
- iaml/actionables/predictors/classifier/act_linear_svc.py +134 -0
- iaml/actionables/predictors/classifier/act_logistic_regression.py +92 -0
- iaml/actionables/predictors/classifier/act_mlp_classifier.py +107 -0
- iaml/actionables/predictors/classifier/act_multinomial_nb.py +76 -0
- iaml/actionables/predictors/classifier/act_passive_aggressive_classifier.py +141 -0
- iaml/actionables/predictors/classifier/act_quadratic_discriminant_analysis.py +72 -0
- iaml/actionables/predictors/classifier/act_randomforest.py +113 -0
- iaml/actionables/predictors/classifier/act_ridge_classifier.py +116 -0
- iaml/actionables/predictors/classifier/act_sgd_classifier.py +149 -0
- iaml/actionables/predictors/classifier/act_svm_svc.py +88 -0
- iaml/actionables/predictors/classifier/act_xgboost.py +111 -0
- iaml/actionables/predictors/regressor/__init__.py +27 -0
- iaml/actionables/predictors/regressor/act_ada_boost_regressor.py +75 -0
- iaml/actionables/predictors/regressor/act_ard_regression.py +95 -0
- iaml/actionables/predictors/regressor/act_catboost_regressor.py +134 -0
- iaml/actionables/predictors/regressor/act_decision_tree_regressor.py +111 -0
- iaml/actionables/predictors/regressor/act_elastic_net_regressor.py +109 -0
- iaml/actionables/predictors/regressor/act_extra_trees_regressor.py +113 -0
- iaml/actionables/predictors/regressor/act_gaussian_process_regressor.py +55 -0
- iaml/actionables/predictors/regressor/act_gboost_regressor.py +95 -0
- iaml/actionables/predictors/regressor/act_hist_gradient_boosting_regressor.py +105 -0
- iaml/actionables/predictors/regressor/act_huber_regressor.py +101 -0
- iaml/actionables/predictors/regressor/act_knn_regressor.py +86 -0
- iaml/actionables/predictors/regressor/act_lasso_regressor.py +103 -0
- iaml/actionables/predictors/regressor/act_light_gbm_regressor.py +201 -0
- iaml/actionables/predictors/regressor/act_linear_regression.py +43 -0
- iaml/actionables/predictors/regressor/act_mlp_regressor.py +104 -0
- iaml/actionables/predictors/regressor/act_poisson_regressor.py +111 -0
- iaml/actionables/predictors/regressor/act_quantile_regressor.py +87 -0
- iaml/actionables/predictors/regressor/act_randomforest_regressor.py +116 -0
- iaml/actionables/predictors/regressor/act_ransac_regressor.py +106 -0
- iaml/actionables/predictors/regressor/act_ridge_regressor.py +107 -0
- iaml/actionables/predictors/regressor/act_sgd_regressor.py +106 -0
- iaml/actionables/predictors/regressor/act_svm_svr.py +81 -0
- iaml/actionables/predictors/regressor/act_xgboost_regressor.py +97 -0
- iaml/actionables/predictors/survival/__init__.py +12 -0
- iaml/actionables/predictors/survival/act_aalen_additive_model.py +83 -0
- iaml/actionables/predictors/survival/act_cox.py +110 -0
- iaml/actionables/predictors/survival/act_coxnet_survival_analysis.py +134 -0
- iaml/actionables/predictors/survival/act_extra_survival_trees.py +101 -0
- iaml/actionables/predictors/survival/act_fast_survival_svm.py +102 -0
- iaml/actionables/predictors/survival/act_gradient_boosting_survival_analysis.py +93 -0
- iaml/actionables/predictors/survival/act_random_survival_forest.py +91 -0
- iaml/actionables/predictors/survival/act_survival_component_wise_gboost.py +80 -0
- iaml/actionables/predictors/survival/act_survival_tree.py +120 -0
- iaml/actionables/predictors/survival/act_survival_xgboost.py +9 -0
- iaml/actionables/predictors/survival/act_weibull_aft.py +230 -0
- iaml/cache.py +61 -0
- iaml/cache_keys.py +57 -0
- iaml/candidate.py +736 -0
- iaml/core_dispatcher.py +125 -0
- iaml/data_type.py +11 -0
- iaml/dataset.py +506 -0
- iaml/decorators/__init__.py +3 -0
- iaml/decorators/all.py +4 -0
- iaml/decorators/is_step.py +45 -0
- iaml/decorators/runner.py +100 -0
- iaml/explanation.py +112 -0
- iaml/iaml.py +1072 -0
- iaml/iaml_pipeline.py +600 -0
- iaml/logger.py +138 -0
- iaml/meta_explorer_step.py +62 -0
- iaml/meta_ordered_step.py +28 -0
- iaml/meta_partial_explorer_step.py +34 -0
- iaml/meta_singleton.py +24 -0
- iaml/metastep.py +211 -0
- iaml/metric.py +111 -0
- iaml/metric_plot.py +82 -0
- iaml/metrics/__init__.py +21 -0
- iaml/metrics/_classification.py +28 -0
- iaml/metrics/_survival_times.py +22 -0
- iaml/metrics/accuracy_metric.py +59 -0
- iaml/metrics/balanced_accuracy_metric.py +67 -0
- iaml/metrics/brier_score.py +90 -0
- iaml/metrics/classification_error_metric.py +66 -0
- iaml/metrics/concordance_index_ipcw.py +84 -0
- iaml/metrics/concordance_index_metric.py +67 -0
- iaml/metrics/cumulative_dynamic_auc.py +119 -0
- iaml/metrics/f1_score_metric.py +71 -0
- iaml/metrics/integrated_brier_score.py +98 -0
- iaml/metrics/integrated_brier_score_loss.py +41 -0
- iaml/metrics/mean_absolute_error_metric.py +46 -0
- iaml/metrics/mean_squared_error_metric.py +46 -0
- iaml/metrics/mean_squared_log_error_metric.py +49 -0
- iaml/metrics/median_absolute_error_metric.py +48 -0
- iaml/metrics/precision_metric.py +63 -0
- iaml/metrics/r2_score_metric.py +45 -0
- iaml/metrics/recall_metric.py +65 -0
- iaml/metrics/roc_auc_metric.py +50 -0
- iaml/metrics/specificity_metric.py +44 -0
- iaml/metrics/specificity_multiclass_metric.py +55 -0
- iaml/metrics/specificity_multilabel_metric.py +60 -0
- iaml/optimizers/__init__.py +5 -0
- iaml/optimizers/bayesian_optimizer.py +193 -0
- iaml/optimizers/genetic_optimizer.py +284 -0
- iaml/optimizers/optimizer.py +31 -0
- iaml/optimizers/random_optimizer.py +101 -0
- iaml/plot.py +138 -0
- iaml/plots/__init__.py +32 -0
- iaml/plots/bar_plot.py +141 -0
- iaml/plots/box_plot.py +166 -0
- iaml/plots/class_prediction_error_plot.py +37 -0
- iaml/plots/classification_report_plot.py +35 -0
- iaml/plots/confusion_matrix_plot.py +34 -0
- iaml/plots/correlation_heatmap_plot.py +201 -0
- iaml/plots/cumulative_hazard_plot.py +72 -0
- iaml/plots/density_plot.py +210 -0
- iaml/plots/histogram_plot.py +179 -0
- iaml/plots/kaplan_meier_comparison_plot.py +89 -0
- iaml/plots/line_plot.py +70 -0
- iaml/plots/missingness_heatmap_plot.py +203 -0
- iaml/plots/outlier_plot.py +217 -0
- iaml/plots/pair_plot.py +228 -0
- iaml/plots/precision_recall_curve_plot.py +86 -0
- iaml/plots/prediction_error_plot.py +34 -0
- iaml/plots/qq_plot.py +220 -0
- iaml/plots/residual_plot.py +38 -0
- iaml/plots/roc_dynamique_curve_plot.py +79 -0
- iaml/plots/rocauc_plot.py +96 -0
- iaml/plots/shap_plot.py +187 -0
- iaml/plots/target_distribution_plot.py +241 -0
- iaml/plots/violin_plot.py +206 -0
- iaml/predictor.py +139 -0
- iaml/reference.py +65 -0
- iaml/shared_cache.py +90 -0
- iaml/sklearn_preprocessor.py +74 -0
- iaml/splitters/__init__.py +3 -0
- iaml/splitters/kfold_splitter.py +32 -0
- iaml/splitters/random_splitter.py +26 -0
- iaml/stack.py +39 -0
- iaml/statistic.py +66 -0
- iaml/statistics/__init__.py +77 -0
- iaml/statistics/anova_statistic.py +80 -0
- iaml/statistics/cardinality_ratio_statistic.py +63 -0
- iaml/statistics/category_cooccurrence_statistic.py +79 -0
- iaml/statistics/chi_square_statistic.py +81 -0
- iaml/statistics/coef_variation_statistic.py +72 -0
- iaml/statistics/correlation_with_target.py +105 -0
- iaml/statistics/count.py +72 -0
- iaml/statistics/data_type_summary_statistic.py +74 -0
- iaml/statistics/duplicate_row_statistic.py +56 -0
- iaml/statistics/effect_size_statistic.py +129 -0
- iaml/statistics/entropy_statistic.py +69 -0
- iaml/statistics/event_rate_statistic.py +52 -0
- iaml/statistics/grouped_mean_statistic.py +60 -0
- iaml/statistics/iqr_statistic.py +66 -0
- iaml/statistics/kurtosis.py +50 -0
- iaml/statistics/mad_statistic.py +66 -0
- iaml/statistics/mean.py +61 -0
- iaml/statistics/median_statistic.py +61 -0
- iaml/statistics/minmax.py +60 -0
- iaml/statistics/missing_rate_statistic.py +62 -0
- iaml/statistics/mode.py +47 -0
- iaml/statistics/most_frequent_ratio.py +81 -0
- iaml/statistics/outlier_count_iqr_statistic.py +76 -0
- iaml/statistics/quantile.py +59 -0
- iaml/statistics/range.py +53 -0
- iaml/statistics/rare_category_rate.py +92 -0
- iaml/statistics/skewness.py +53 -0
- iaml/statistics/stdev.py +50 -0
- iaml/statistics/summary_table_statistic.py +60 -0
- iaml/statistics/time_by_group_statistic.py +83 -0
- iaml/statistics/time_summary_statistic.py +56 -0
- iaml/statistics/top_k_value_counts.py +68 -0
- iaml/statistics/unique_count_statistic.py +57 -0
- iaml/statistics/value_counts.py +63 -0
- iaml/statistics/variance.py +51 -0
- iaml/statistics/violin.py +63 -0
- iaml/step.py +600 -0
- iaml/step_cache.py +87 -0
- iaml/step_wrapper.py +79 -0
- iaml/timed_pool_executor.py +492 -0
- iaml/type_of_target.py +68 -0
- iaml/void_step.py +101 -0
- iaml/worker_manager.py +169 -0
- iaml/wrapper/__init__.py +4 -0
- iaml/wrapper/wrap_basic_gridsearch.py +68 -0
- iaml/wrapper/wrap_genetic_gridsearch.py +293 -0
- iaml/wrapper/wrap_iterative_gridsearch.py +399 -0
- pyiaml-1.0.0.dist-info/METADATA +802 -0
- pyiaml-1.0.0.dist-info/RECORD +279 -0
- pyiaml-1.0.0.dist-info/WHEEL +5 -0
- pyiaml-1.0.0.dist-info/licenses/LICENSE +674 -0
- pyiaml-1.0.0.dist-info/top_level.txt +1 -0
iaml/step_cache.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Shared in-process cache for step outputs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from threading import RLock
|
|
6
|
+
from typing import Any
|
|
7
|
+
from weakref import ReferenceType, ref
|
|
8
|
+
|
|
9
|
+
from .meta_singleton import MetaSingleton
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StepCache(metaclass=MetaSingleton):
|
|
13
|
+
"""LRU cache shared across deep-copied steps, validating input identity."""
|
|
14
|
+
def __init__(self, max_size: int = 1000) -> None:
|
|
15
|
+
self._max_size = max_size
|
|
16
|
+
self._data: OrderedDict[tuple, tuple[ReferenceType | None, Any]] = OrderedDict()
|
|
17
|
+
self._by_step: dict[str, set[tuple]] = {}
|
|
18
|
+
self._lock = RLock()
|
|
19
|
+
|
|
20
|
+
def get(self, key: tuple, input_candidate: Any) -> Any | None:
|
|
21
|
+
"""Return an output only while its original input still matches by identity."""
|
|
22
|
+
with self._lock:
|
|
23
|
+
entry = self._data.get(key)
|
|
24
|
+
if entry is None:
|
|
25
|
+
return None
|
|
26
|
+
input_ref, value = entry
|
|
27
|
+
original_input = input_ref() if input_ref is not None else None
|
|
28
|
+
if original_input is not input_candidate or (
|
|
29
|
+
input_ref is not None and original_input is None
|
|
30
|
+
):
|
|
31
|
+
self._remove(key)
|
|
32
|
+
return None
|
|
33
|
+
if value is None:
|
|
34
|
+
return None
|
|
35
|
+
self._data.move_to_end(key)
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
def put(self, key: tuple, value: Any, step_cache_id: str, input_candidate: Any) -> None:
|
|
39
|
+
"""Cache an output without keeping its input candidate alive."""
|
|
40
|
+
with self._lock:
|
|
41
|
+
input_ref = ref(input_candidate) if input_candidate is not None else None
|
|
42
|
+
if key in self._data:
|
|
43
|
+
self._data.move_to_end(key)
|
|
44
|
+
else:
|
|
45
|
+
self._by_step.setdefault(step_cache_id, set()).add(key)
|
|
46
|
+
self._data[key] = (input_ref, value)
|
|
47
|
+
self._evict()
|
|
48
|
+
|
|
49
|
+
def _remove(self, key: tuple) -> None:
|
|
50
|
+
"""Remove an entry and its step index while holding the cache lock."""
|
|
51
|
+
self._data.pop(key, None)
|
|
52
|
+
step_cache_id = key[0]
|
|
53
|
+
keys = self._by_step.get(step_cache_id)
|
|
54
|
+
if keys is not None:
|
|
55
|
+
keys.discard(key)
|
|
56
|
+
if not keys:
|
|
57
|
+
self._by_step.pop(step_cache_id, None)
|
|
58
|
+
|
|
59
|
+
def _evict(self) -> None:
|
|
60
|
+
while len(self._data) > self._max_size:
|
|
61
|
+
self._remove(next(iter(self._data)))
|
|
62
|
+
|
|
63
|
+
def clear(self, step_cache_id: str) -> None:
|
|
64
|
+
with self._lock:
|
|
65
|
+
keys = self._by_step.pop(step_cache_id, None)
|
|
66
|
+
if not keys:
|
|
67
|
+
return
|
|
68
|
+
for key in keys:
|
|
69
|
+
self._data.pop(key, None)
|
|
70
|
+
|
|
71
|
+
def values_for_step(self, step_cache_id: str) -> list[Any]:
|
|
72
|
+
with self._lock:
|
|
73
|
+
if step_cache_id not in self._by_step:
|
|
74
|
+
return []
|
|
75
|
+
return [
|
|
76
|
+
value for key, (_, value) in self._data.items()
|
|
77
|
+
if key[0] == step_cache_id
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def size_for_step(self, step_cache_id: str) -> int:
|
|
81
|
+
with self._lock:
|
|
82
|
+
keys = self._by_step.get(step_cache_id)
|
|
83
|
+
return len(keys) if keys else 0
|
|
84
|
+
|
|
85
|
+
def total_size(self) -> int:
|
|
86
|
+
with self._lock:
|
|
87
|
+
return len(self._data)
|
iaml/step_wrapper.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""StepWrapper is a direct child of Step and will wrap and execute another step.
|
|
2
|
+
Wrap with StepWrapper is useless, use children classes
|
|
3
|
+
"""
|
|
4
|
+
from .step import Step
|
|
5
|
+
from .decorators.all import is_step, runner
|
|
6
|
+
from .candidate import Candidate
|
|
7
|
+
from .dataset import Dataset
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@is_step('wrapper')
|
|
11
|
+
class StepWrapper(Step):
|
|
12
|
+
"""StepWrapper is a direct child of Step and will wrap and execute another step.
|
|
13
|
+
Wrap with StepWrapper is useless, use children classes
|
|
14
|
+
"""
|
|
15
|
+
_usage = "Use when you need a minimal wrapper around exactly one child step for delegation. Applicable to any dataset and single-step pipelines. Avoid when Actionable or MetaStep provides the intended behavior."
|
|
16
|
+
|
|
17
|
+
def __init__(self, step: Step, **kwargs): # pylint: disable=unused-argument
|
|
18
|
+
# is_step forwards subclass constructor options through every parent.
|
|
19
|
+
self.step: Step = step
|
|
20
|
+
"""The step to wrap"""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_pipeline(cls, pipeline: dict, *args, **kwargs) -> Step:
|
|
24
|
+
"""Load any kind of StepWrapper. The step must have exactly one child
|
|
25
|
+
|
|
26
|
+
:param dict pipeline: JSON pipeline.
|
|
27
|
+
:raise TypeError: invalid pipeline: StepWrapper must have exactly one child.
|
|
28
|
+
:return: Loaded step.
|
|
29
|
+
"""
|
|
30
|
+
if 'children' not in pipeline or len(pipeline['children']) != 1:
|
|
31
|
+
raise TypeError('invalid pipeline: StepWrapper must have exactly one child')
|
|
32
|
+
|
|
33
|
+
child = Step.from_pipeline(pipeline['children'][0])
|
|
34
|
+
step = super().from_pipeline(pipeline, child, *args, **kwargs)
|
|
35
|
+
|
|
36
|
+
return step
|
|
37
|
+
|
|
38
|
+
def configure_parents(self, *parents) -> None:
|
|
39
|
+
self.step.configure_parents(*parents)
|
|
40
|
+
super().configure_parents(*parents)
|
|
41
|
+
|
|
42
|
+
def wrap(self, step: Step) -> None:
|
|
43
|
+
"""Set wrapped step
|
|
44
|
+
|
|
45
|
+
:param Step step: Step to wrap.
|
|
46
|
+
:raise ValueError: Step must be an occurrence of step (or inherited classes).
|
|
47
|
+
"""
|
|
48
|
+
if Step in step.__class__.__mro__:
|
|
49
|
+
self.step = step
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError("Step must be an occurrence of step (or inherited classes)")
|
|
52
|
+
|
|
53
|
+
def suitable(self, dataset: Dataset) -> bool:
|
|
54
|
+
return self.step.suitable(dataset)
|
|
55
|
+
|
|
56
|
+
def all_configurations(self) -> list[dict]:
|
|
57
|
+
to_return = Step.all_configurations(self)
|
|
58
|
+
to_return = to_return + self.step.all_configurations()
|
|
59
|
+
|
|
60
|
+
return to_return
|
|
61
|
+
|
|
62
|
+
def json_pipeline(self) -> dict:
|
|
63
|
+
return {
|
|
64
|
+
**super().json_pipeline(),
|
|
65
|
+
'children': [self.step.json_pipeline()]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def all_steps(self) -> list[Step]:
|
|
69
|
+
return [self.step, *self.step.all_steps()]
|
|
70
|
+
|
|
71
|
+
@runner
|
|
72
|
+
def run(self, candidate: Candidate) -> Candidate:
|
|
73
|
+
return self.step.run(candidate)
|
|
74
|
+
|
|
75
|
+
def count_steps(self) -> int:
|
|
76
|
+
return 1 + self.step.count_steps()
|
|
77
|
+
|
|
78
|
+
def priorize(self, candidate: Candidate=None) -> float:
|
|
79
|
+
return self.step.priorize(candidate)
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""TimedPoolExecutor will run *max_workers* new process and will send them actions
|
|
2
|
+
to run.
|
|
3
|
+
Compare to ProcessPoolExecutor, this one allow us to kill process quickly after timeout
|
|
4
|
+
"""
|
|
5
|
+
import random
|
|
6
|
+
import signal
|
|
7
|
+
import time
|
|
8
|
+
import traceback
|
|
9
|
+
import warnings
|
|
10
|
+
import threading
|
|
11
|
+
import queue
|
|
12
|
+
import pickle
|
|
13
|
+
import multiprocess
|
|
14
|
+
import multiprocess.managers
|
|
15
|
+
import multiprocess.process
|
|
16
|
+
|
|
17
|
+
from .logger import Logger
|
|
18
|
+
from .core_dispatcher import CoreDispatcher
|
|
19
|
+
from .shared_cache import start_cache_manager
|
|
20
|
+
from .cache import Cache
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TerminatedError(RuntimeError):
|
|
24
|
+
"""Custom RuntimeError
|
|
25
|
+
Raised when we try to run a job in a stopped executor
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def process_daemon(
|
|
30
|
+
to_run_queue: multiprocess.Queue,
|
|
31
|
+
queue: multiprocess.Queue,
|
|
32
|
+
error_queue: multiprocess.Queue,
|
|
33
|
+
finally_queue: multiprocess.Queue,
|
|
34
|
+
shared_cache) -> None:
|
|
35
|
+
"""Will be run by TimedPoolExecutor -> Daemon process able to handle actions
|
|
36
|
+
|
|
37
|
+
:param multiprocess.Queue to_run_queue: List of action to run
|
|
38
|
+
:param multiprocess.Queue queue: Queue used to send result
|
|
39
|
+
:param multiprocess.Queue error_queue: Queue used to raise errors
|
|
40
|
+
:param multiprocess.Queue finally_queue: Queue used for every run (success or fail).
|
|
41
|
+
Used to count number of ran actions
|
|
42
|
+
"""
|
|
43
|
+
result = None
|
|
44
|
+
|
|
45
|
+
Cache().configure(shared_cache)
|
|
46
|
+
|
|
47
|
+
time.sleep(random.random()) # Weird thing to un-sync the threads
|
|
48
|
+
|
|
49
|
+
with warnings.catch_warnings():
|
|
50
|
+
warnings.filterwarnings("ignore")
|
|
51
|
+
while True:
|
|
52
|
+
value = to_run_queue.get()
|
|
53
|
+
|
|
54
|
+
method, args, kwargs, callback_id = value
|
|
55
|
+
try:
|
|
56
|
+
result = method(*args, **kwargs)
|
|
57
|
+
queue.put((result, callback_id))
|
|
58
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
59
|
+
error_queue.put((traceback.format_exc(), callback_id))
|
|
60
|
+
finally:
|
|
61
|
+
finally_queue.put(1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TimedPoolExecutor: # pylint: disable=too-many-instance-attributes
|
|
65
|
+
"""TimedPoolExecutor will run *max_workers* new process and will send them actions
|
|
66
|
+
to run.
|
|
67
|
+
Compare to ProcessPoolExecutor, this one allow us to kill process quickly after timeout
|
|
68
|
+
|
|
69
|
+
:param int, optional max_workers: Maximum number of parallel workers
|
|
70
|
+
:param callable, optional callback: Function call when the worker is done
|
|
71
|
+
:param bool, optional sliding_stages: Wait for all workers to end, or not
|
|
72
|
+
:param bool, optional debug: Are we in debug mode ?
|
|
73
|
+
"""
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
max_workers: int = None,
|
|
77
|
+
callback: callable = None,
|
|
78
|
+
sliding_stages: bool = True,
|
|
79
|
+
debug: bool = False) -> None:
|
|
80
|
+
"""Initialize a TimedPoolExecutor
|
|
81
|
+
"""
|
|
82
|
+
self.max_workers: int = min(max_workers, multiprocess.cpu_count())
|
|
83
|
+
"""Maximum number of workers allowed to work in parallel"""
|
|
84
|
+
|
|
85
|
+
self.debug: bool = debug
|
|
86
|
+
"""If true, task will be done without using any process. Easier to debug"""
|
|
87
|
+
|
|
88
|
+
self.stop_flag: bool = False
|
|
89
|
+
"""Used to stop thread"""
|
|
90
|
+
|
|
91
|
+
self.sliding_stages: bool = sliding_stages
|
|
92
|
+
"""If True, don't wait for all workers to end, leaving empty cpu cores"""
|
|
93
|
+
|
|
94
|
+
# Daemon THREAD (& not Process)
|
|
95
|
+
self.main_daemon: threading.Thread = None
|
|
96
|
+
"""Main runnng thread with a infinite loop to catch results of sub process"""
|
|
97
|
+
|
|
98
|
+
self.daemons_collectors: list[threading.Thread] = None
|
|
99
|
+
"""List of running daemons"""
|
|
100
|
+
|
|
101
|
+
self._mp_capable: bool = True
|
|
102
|
+
"""Flag indicating whether multiprocessing primitives are available."""
|
|
103
|
+
self._mp_fallback: bool = False
|
|
104
|
+
"""Flag indicating whether we already fell back to sequential execution."""
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
self.manager: multiprocess.Manager = multiprocess.Manager()
|
|
108
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
109
|
+
warnings.warn(f"TimedPoolExecutor fallback to sequential mode (manager start failed: {exc!r})")
|
|
110
|
+
self.manager = None
|
|
111
|
+
self._mp_capable = False
|
|
112
|
+
|
|
113
|
+
if self._mp_capable:
|
|
114
|
+
self.to_run_queue = self.manager.Queue()
|
|
115
|
+
self.error_queue = self.manager.Queue()
|
|
116
|
+
self.result_queue = self.manager.Queue()
|
|
117
|
+
self.finally_queue = self.manager.Queue()
|
|
118
|
+
else:
|
|
119
|
+
self.to_run_queue = queue.Queue()
|
|
120
|
+
self.error_queue = queue.Queue()
|
|
121
|
+
self.result_queue = queue.Queue()
|
|
122
|
+
self.finally_queue = queue.Queue()
|
|
123
|
+
# Queues used to exchange data with subprocesses
|
|
124
|
+
|
|
125
|
+
self.cache_manager: multiprocess.managers.BaseManager | None = None
|
|
126
|
+
"""Keep a strong reference to the shared cache manager process"""
|
|
127
|
+
|
|
128
|
+
self.shared_cache = None
|
|
129
|
+
"""Proxy object used by workers to talk to the shared cache"""
|
|
130
|
+
|
|
131
|
+
self.callbacks: list[callable] = [callback]
|
|
132
|
+
"""Method to call after each run"""
|
|
133
|
+
|
|
134
|
+
self.results: list = []
|
|
135
|
+
"""List of all result since last reset"""
|
|
136
|
+
|
|
137
|
+
self.submit_count: int = 0
|
|
138
|
+
"""Count -> Help TimedPoolExecutor to know if everything is finished"""
|
|
139
|
+
|
|
140
|
+
self.finished_run: int = 0
|
|
141
|
+
"""Count -> Help TimedPoolExecutor to know if everything is finished"""
|
|
142
|
+
|
|
143
|
+
self.process: list[multiprocess.Process] = []
|
|
144
|
+
"""List of sub processes"""
|
|
145
|
+
|
|
146
|
+
if not self._mp_capable:
|
|
147
|
+
self.debug = True
|
|
148
|
+
self.max_workers = 1
|
|
149
|
+
self.cache_manager = None
|
|
150
|
+
self.shared_cache = None
|
|
151
|
+
Cache().configure(None)
|
|
152
|
+
else:
|
|
153
|
+
try:
|
|
154
|
+
self.cache_manager, self.shared_cache = start_cache_manager(max_cache_size=500)
|
|
155
|
+
Cache().configure(self.shared_cache)
|
|
156
|
+
except OSError as exc:
|
|
157
|
+
warnings.warn(f"Shared cache disabled (start_cache_manager failed: {exc!r})")
|
|
158
|
+
self.cache_manager = None
|
|
159
|
+
self.shared_cache = None
|
|
160
|
+
Cache().configure(None)
|
|
161
|
+
|
|
162
|
+
# Create and start sub process (will only wait until first submit)
|
|
163
|
+
for _ in range(max_workers):
|
|
164
|
+
self.process.append(
|
|
165
|
+
multiprocess.Process( # pylint: disable=not-callable
|
|
166
|
+
target=process_daemon,
|
|
167
|
+
args=[self.to_run_queue,
|
|
168
|
+
self.result_queue,
|
|
169
|
+
self.error_queue,
|
|
170
|
+
self.finally_queue,
|
|
171
|
+
self.shared_cache
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
self.process[-1].start()
|
|
176
|
+
|
|
177
|
+
CoreDispatcher().affiliate(
|
|
178
|
+
[process.pid for process in self.process],
|
|
179
|
+
core_number=self.max_workers)
|
|
180
|
+
|
|
181
|
+
self.__run_daemon() # Run the daemon THREAD
|
|
182
|
+
|
|
183
|
+
if threading.current_thread() is threading.main_thread():
|
|
184
|
+
signal.signal(signal.SIGINT, lambda *_: self.shutdown())
|
|
185
|
+
signal.signal(signal.SIGTERM, lambda *_: self.shutdown())
|
|
186
|
+
|
|
187
|
+
def __del__(self):
|
|
188
|
+
"""When delete -> TimedPoolExecutor kill all these daemons
|
|
189
|
+
"""
|
|
190
|
+
self.shutdown()
|
|
191
|
+
|
|
192
|
+
def shutdown(self) -> None:
|
|
193
|
+
"""Shutdown TimedPoolExecutor : Kill subprocess and thread
|
|
194
|
+
"""
|
|
195
|
+
self.stop_flag = True # Main daemon thread will kill process
|
|
196
|
+
if self.main_daemon:
|
|
197
|
+
self.main_daemon.join()
|
|
198
|
+
|
|
199
|
+
def __collect_results(self) -> None:
|
|
200
|
+
"""Collect results from queues and run callback
|
|
201
|
+
"""
|
|
202
|
+
while True:
|
|
203
|
+
result, callback_id = self.result_queue.get()
|
|
204
|
+
|
|
205
|
+
if isinstance(result, str) and result == 'stop':
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
if callback_id is not None and callable(self.callbacks[callback_id]):
|
|
209
|
+
self.callbacks[callback_id](result)
|
|
210
|
+
|
|
211
|
+
Logger().info(str(result))
|
|
212
|
+
self.results.append(result)
|
|
213
|
+
|
|
214
|
+
def __collect_finally(self) -> None:
|
|
215
|
+
while True:
|
|
216
|
+
item = self.finally_queue.get()
|
|
217
|
+
if item == "stop":
|
|
218
|
+
break
|
|
219
|
+
self.finished_run += 1
|
|
220
|
+
|
|
221
|
+
def __print_errors(self) -> None:
|
|
222
|
+
"""Collect and print error from error_queue"""
|
|
223
|
+
while True:
|
|
224
|
+
item = self.error_queue.get()
|
|
225
|
+
|
|
226
|
+
if item is None:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
error, callback_id = item
|
|
230
|
+
|
|
231
|
+
if error == 'stop':
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
Logger().error("Error in a subprocess : ", error)
|
|
235
|
+
if callback_id is not None and callback_id < len(self.callbacks):
|
|
236
|
+
callback = self.callbacks[callback_id]
|
|
237
|
+
if callable(callback):
|
|
238
|
+
callback(None)
|
|
239
|
+
|
|
240
|
+
def __keep_running(self) -> None:
|
|
241
|
+
"""Daemon THREAD process. Infinite loop to catch results & errors"""
|
|
242
|
+
while True:
|
|
243
|
+
if self.stop_flag:
|
|
244
|
+
self.error_queue.put(("stop", None)) # Gentilly ask thread to stop
|
|
245
|
+
self.result_queue.put(("stop", None)) # Gentilly ask thread to stop
|
|
246
|
+
self.finally_queue.put("stop") # Gentilly ask thread to stop
|
|
247
|
+
|
|
248
|
+
for process in self.process:
|
|
249
|
+
process.kill()
|
|
250
|
+
|
|
251
|
+
# empty task queue
|
|
252
|
+
while not self.to_run_queue.empty():
|
|
253
|
+
self.to_run_queue.get()
|
|
254
|
+
|
|
255
|
+
if self.manager is not None:
|
|
256
|
+
self.manager.shutdown()
|
|
257
|
+
if self.cache_manager is not None:
|
|
258
|
+
self.cache_manager.shutdown()
|
|
259
|
+
self.cache_manager = None
|
|
260
|
+
|
|
261
|
+
self.shared_cache = None
|
|
262
|
+
Cache().configure(None)
|
|
263
|
+
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
Logger().print_queue()
|
|
267
|
+
time.sleep(0.5)
|
|
268
|
+
|
|
269
|
+
def __run_daemon(self) -> None:
|
|
270
|
+
"""Start the daemon THREAD"""
|
|
271
|
+
self.stop_flag = False
|
|
272
|
+
if not self.main_daemon or not self.main_daemon.is_alive():
|
|
273
|
+
self.main_daemon = threading.Thread(target=self.__keep_running)
|
|
274
|
+
self.main_daemon.start()
|
|
275
|
+
|
|
276
|
+
self.__run_collectors()
|
|
277
|
+
|
|
278
|
+
def __run_collectors(self) -> None:
|
|
279
|
+
"""Start collector daemons"""
|
|
280
|
+
self.daemons_collectors = [
|
|
281
|
+
threading.Thread(target=self.__print_errors),
|
|
282
|
+
threading.Thread(target=self.__collect_results),
|
|
283
|
+
threading.Thread(target=self.__collect_finally)]
|
|
284
|
+
|
|
285
|
+
for collector in self.daemons_collectors:
|
|
286
|
+
collector.start()
|
|
287
|
+
|
|
288
|
+
def _drain_queue(self, target_queue: queue.Queue | multiprocess.managers.BaseProxy) -> None:
|
|
289
|
+
"""Clear queued tasks without blocking."""
|
|
290
|
+
while True:
|
|
291
|
+
try:
|
|
292
|
+
target_queue.get_nowait()
|
|
293
|
+
except Exception:
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
def _terminate_workers(self) -> None:
|
|
297
|
+
"""Stop all worker processes immediately."""
|
|
298
|
+
for process in self.process:
|
|
299
|
+
try:
|
|
300
|
+
if process.is_alive():
|
|
301
|
+
process.kill()
|
|
302
|
+
except Exception:
|
|
303
|
+
try:
|
|
304
|
+
process.terminate()
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
try:
|
|
308
|
+
process.join(timeout=0.2)
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
self.process = []
|
|
312
|
+
|
|
313
|
+
def _restart_workers(self) -> None:
|
|
314
|
+
"""Restart worker processes after a timeout cancellation."""
|
|
315
|
+
if not self._mp_capable:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
for _ in range(self.max_workers):
|
|
319
|
+
self.process.append(
|
|
320
|
+
multiprocess.Process( # pylint: disable=not-callable
|
|
321
|
+
target=process_daemon,
|
|
322
|
+
args=[self.to_run_queue,
|
|
323
|
+
self.result_queue,
|
|
324
|
+
self.error_queue,
|
|
325
|
+
self.finally_queue,
|
|
326
|
+
self.shared_cache
|
|
327
|
+
]
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
self.process[-1].start()
|
|
331
|
+
|
|
332
|
+
CoreDispatcher().affiliate(
|
|
333
|
+
[process.pid for process in self.process],
|
|
334
|
+
core_number=self.max_workers)
|
|
335
|
+
|
|
336
|
+
def submit(self, target: callable, *args, deadline: float | None = None, **kwargs) -> bool:
|
|
337
|
+
"""Submit a task, waiting for space when a deadline is specified.
|
|
338
|
+
|
|
339
|
+
:param callable target: Method to run
|
|
340
|
+
:param Tuple, optional args: parameters passed to the callable
|
|
341
|
+
:param float, optional deadline: Absolute time from ``time.monotonic()``.
|
|
342
|
+
:param Dict, optional kwargs: parameters passed to the callable
|
|
343
|
+
:return: False if the deadline expires before submission, otherwise True.
|
|
344
|
+
"""
|
|
345
|
+
while True:
|
|
346
|
+
if self.stop_flag:
|
|
347
|
+
raise TerminatedError("Job submission failed: Executor is currently \
|
|
348
|
+
shutdown and cannot accept new tasks.")
|
|
349
|
+
|
|
350
|
+
remaining = float("inf") if deadline is None else deadline - time.monotonic()
|
|
351
|
+
if remaining <= 0:
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
# Keep at most one waiting task per worker in addition to those running.
|
|
355
|
+
# Large candidate objects otherwise make both submission and cancellation
|
|
356
|
+
# spend most of the training budget serializing an unbounded backlog.
|
|
357
|
+
if deadline is None or self.submit_count - self.finished_run < self.max_workers * 2:
|
|
358
|
+
break
|
|
359
|
+
time.sleep(min(0.05, remaining))
|
|
360
|
+
|
|
361
|
+
callback_id = len(self.callbacks) - 1
|
|
362
|
+
|
|
363
|
+
if self.debug:
|
|
364
|
+
result = target(*args, **kwargs)
|
|
365
|
+
if callback_id is not None and callable(self.callbacks[callback_id]):
|
|
366
|
+
self.callbacks[callback_id](result)
|
|
367
|
+
Logger().info(str(result))
|
|
368
|
+
self.results.append(result)
|
|
369
|
+
self.submit_count += 1
|
|
370
|
+
self.finished_run += 1
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
self.to_run_queue.put((target, args, kwargs, callback_id))
|
|
375
|
+
self.submit_count += 1
|
|
376
|
+
except pickle.PicklingError as exc:
|
|
377
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
378
|
+
return False
|
|
379
|
+
if not self._mp_fallback:
|
|
380
|
+
warnings.warn(
|
|
381
|
+
f"TimedPoolExecutor fallback to sequential mode (pickle failed: {exc!r})"
|
|
382
|
+
)
|
|
383
|
+
self._mp_fallback = True
|
|
384
|
+
self.debug = True
|
|
385
|
+
result = target(*args, **kwargs)
|
|
386
|
+
if callback_id is not None and callable(self.callbacks[callback_id]):
|
|
387
|
+
self.callbacks[callback_id](result)
|
|
388
|
+
Logger().info(str(result))
|
|
389
|
+
self.results.append(result)
|
|
390
|
+
self.submit_count += 1
|
|
391
|
+
self.finished_run += 1
|
|
392
|
+
|
|
393
|
+
return True
|
|
394
|
+
|
|
395
|
+
def __finished(self) -> bool:
|
|
396
|
+
"""Are all the submitted tasks finished?
|
|
397
|
+
|
|
398
|
+
:return: True if all tasks are finished
|
|
399
|
+
"""
|
|
400
|
+
return self.finished_run >= self.submit_count
|
|
401
|
+
|
|
402
|
+
def reset(self):
|
|
403
|
+
"""Reset all queues, callback, results, etc.
|
|
404
|
+
Allow to reuse this instance of TimedPoolExecutor without restarting subProcess
|
|
405
|
+
"""
|
|
406
|
+
if not self.sliding_stages:
|
|
407
|
+
self.callbacks = [self.callbacks[-1]]
|
|
408
|
+
self.submit_count = 0
|
|
409
|
+
self.finished_run = 0
|
|
410
|
+
|
|
411
|
+
self.results = []
|
|
412
|
+
|
|
413
|
+
def __join_collectors(self):
|
|
414
|
+
"""Join collector thread.
|
|
415
|
+
Stop and start thread, used when we want to sync with thread to collect all data
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
# Stop and join collector
|
|
419
|
+
self.error_queue.put(("stop", None))
|
|
420
|
+
self.result_queue.put(("stop", None))
|
|
421
|
+
self.finally_queue.put("stop")
|
|
422
|
+
|
|
423
|
+
for collector in self.daemons_collectors:
|
|
424
|
+
collector.join()
|
|
425
|
+
|
|
426
|
+
# Restart collectors
|
|
427
|
+
self.__run_collectors()
|
|
428
|
+
|
|
429
|
+
def set_callback(self, callback: callable) -> None:
|
|
430
|
+
"""Set the method call to when a task finish
|
|
431
|
+
|
|
432
|
+
:param callable callback: callback method
|
|
433
|
+
"""
|
|
434
|
+
self.callbacks.append(callback)
|
|
435
|
+
|
|
436
|
+
def join(self, timeout: float | None, reset: bool = True) -> list:
|
|
437
|
+
"""Wait until all the task are finished or timeout is reach
|
|
438
|
+
If timeout is reach -> Remaining tasks will be kill without sending results
|
|
439
|
+
|
|
440
|
+
:param float timeout: Maximum seconds to wait; None waits without a timeout.
|
|
441
|
+
:param bool, optional reset: Reset the instance after join(). Defaults to True.
|
|
442
|
+
|
|
443
|
+
:return: All finished task results
|
|
444
|
+
"""
|
|
445
|
+
start_time = time.monotonic()
|
|
446
|
+
def remain_time():
|
|
447
|
+
if timeout is None:
|
|
448
|
+
return float("inf")
|
|
449
|
+
return max(0.0, timeout - (time.monotonic() - start_time))
|
|
450
|
+
|
|
451
|
+
def slide():
|
|
452
|
+
try:
|
|
453
|
+
is_empty = self.to_run_queue.empty()
|
|
454
|
+
except BrokenPipeError:
|
|
455
|
+
is_empty = True
|
|
456
|
+
|
|
457
|
+
return self.sliding_stages \
|
|
458
|
+
and (
|
|
459
|
+
is_empty # submit queue is empty
|
|
460
|
+
and (
|
|
461
|
+
self.submit_count - self.finished_run <= self.max_workers/2
|
|
462
|
+
# At least half of the worker is free
|
|
463
|
+
)
|
|
464
|
+
and self.results # We have got at least one result
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
while not self.__finished():
|
|
468
|
+
remaining = remain_time()
|
|
469
|
+
if remaining <= 0 or slide():
|
|
470
|
+
break
|
|
471
|
+
time.sleep(min(0.05, remaining))
|
|
472
|
+
|
|
473
|
+
timed_out = not self.__finished() and remain_time() == 0
|
|
474
|
+
if timed_out:
|
|
475
|
+
if self._mp_capable and self.process:
|
|
476
|
+
self._terminate_workers()
|
|
477
|
+
self._drain_queue(self.to_run_queue)
|
|
478
|
+
|
|
479
|
+
# Join collector thread, just to be sure we have collected all data
|
|
480
|
+
self.__join_collectors()
|
|
481
|
+
|
|
482
|
+
if timed_out:
|
|
483
|
+
self.submit_count = self.finished_run
|
|
484
|
+
if self._mp_capable:
|
|
485
|
+
self._restart_workers()
|
|
486
|
+
|
|
487
|
+
results = self.results # Save before reset!
|
|
488
|
+
|
|
489
|
+
if reset:
|
|
490
|
+
self.reset()
|
|
491
|
+
|
|
492
|
+
return results
|