scitex 2.0.0__py2.py3-none-any.whl → 2.1.0__py2.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.
- scitex/__init__.py +53 -15
- scitex/__main__.py +72 -26
- scitex/__version__.py +1 -1
- scitex/_sh.py +145 -23
- scitex/ai/__init__.py +30 -16
- scitex/ai/_gen_ai/_Anthropic.py +5 -7
- scitex/ai/_gen_ai/_BaseGenAI.py +2 -2
- scitex/ai/_gen_ai/_DeepSeek.py +10 -2
- scitex/ai/_gen_ai/_Google.py +2 -2
- scitex/ai/_gen_ai/_Llama.py +2 -2
- scitex/ai/_gen_ai/_OpenAI.py +2 -2
- scitex/ai/_gen_ai/_PARAMS.py +51 -65
- scitex/ai/_gen_ai/_Perplexity.py +2 -2
- scitex/ai/_gen_ai/__init__.py +25 -14
- scitex/ai/_gen_ai/_format_output_func.py +4 -4
- scitex/ai/classification/{classifier_server.py → Classifier.py} +5 -5
- scitex/ai/classification/CrossValidationExperiment.py +374 -0
- scitex/ai/classification/__init__.py +43 -4
- scitex/ai/classification/reporters/_BaseClassificationReporter.py +281 -0
- scitex/ai/classification/reporters/_ClassificationReporter.py +773 -0
- scitex/ai/classification/reporters/_MultiClassificationReporter.py +406 -0
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +1834 -0
- scitex/ai/classification/reporters/__init__.py +11 -0
- scitex/ai/classification/reporters/reporter_utils/_Plotter.py +1028 -0
- scitex/ai/classification/reporters/reporter_utils/__init__.py +80 -0
- scitex/ai/classification/reporters/reporter_utils/aggregation.py +457 -0
- scitex/ai/classification/reporters/reporter_utils/data_models.py +313 -0
- scitex/ai/classification/reporters/reporter_utils/reporting.py +1056 -0
- scitex/ai/classification/reporters/reporter_utils/storage.py +221 -0
- scitex/ai/classification/reporters/reporter_utils/validation.py +395 -0
- scitex/ai/classification/timeseries/_TimeSeriesBlockingSplit.py +568 -0
- scitex/ai/classification/timeseries/_TimeSeriesCalendarSplit.py +688 -0
- scitex/ai/classification/timeseries/_TimeSeriesMetadata.py +139 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +1716 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit_v01-not-using-n_splits.py +1685 -0
- scitex/ai/classification/timeseries/_TimeSeriesStrategy.py +84 -0
- scitex/ai/classification/timeseries/_TimeSeriesStratifiedSplit.py +610 -0
- scitex/ai/classification/timeseries/__init__.py +39 -0
- scitex/ai/classification/timeseries/_normalize_timestamp.py +436 -0
- scitex/ai/clustering/_umap.py +2 -2
- scitex/ai/feature_extraction/vit.py +1 -0
- scitex/ai/feature_selection/__init__.py +30 -0
- scitex/ai/feature_selection/feature_selection.py +364 -0
- scitex/ai/loss/multi_task_loss.py +1 -1
- scitex/ai/metrics/__init__.py +51 -4
- scitex/ai/metrics/_calc_bacc.py +61 -0
- scitex/ai/metrics/_calc_bacc_from_conf_mat.py +38 -0
- scitex/ai/metrics/_calc_clf_report.py +78 -0
- scitex/ai/metrics/_calc_conf_mat.py +93 -0
- scitex/ai/metrics/_calc_feature_importance.py +183 -0
- scitex/ai/metrics/_calc_mcc.py +61 -0
- scitex/ai/metrics/_calc_pre_rec_auc.py +116 -0
- scitex/ai/metrics/_calc_roc_auc.py +110 -0
- scitex/ai/metrics/_calc_seizure_prediction_metrics.py +490 -0
- scitex/ai/metrics/{silhoute_score_block.py → _calc_silhouette_score.py} +15 -8
- scitex/ai/metrics/_normalize_labels.py +83 -0
- scitex/ai/plt/__init__.py +47 -8
- scitex/ai/plt/{_conf_mat.py → _plot_conf_mat.py} +158 -87
- scitex/ai/plt/_plot_feature_importance.py +323 -0
- scitex/ai/plt/_plot_learning_curve.py +345 -0
- scitex/ai/plt/_plot_optuna_study.py +225 -0
- scitex/ai/plt/_plot_pre_rec_curve.py +290 -0
- scitex/ai/plt/_plot_roc_curve.py +255 -0
- scitex/ai/training/{learning_curve_logger.py → _LearningCurveLogger.py} +197 -213
- scitex/ai/training/__init__.py +2 -2
- scitex/ai/utils/grid_search.py +3 -3
- scitex/benchmark/__init__.py +52 -0
- scitex/benchmark/benchmark.py +400 -0
- scitex/benchmark/monitor.py +370 -0
- scitex/benchmark/profiler.py +297 -0
- scitex/browser/__init__.py +48 -0
- scitex/browser/automation/CookieHandler.py +216 -0
- scitex/browser/automation/__init__.py +7 -0
- scitex/browser/collaboration/__init__.py +55 -0
- scitex/browser/collaboration/auth_helpers.py +94 -0
- scitex/browser/collaboration/collaborative_agent.py +136 -0
- scitex/browser/collaboration/credential_manager.py +188 -0
- scitex/browser/collaboration/interactive_panel.py +400 -0
- scitex/browser/collaboration/persistent_browser.py +170 -0
- scitex/browser/collaboration/shared_session.py +383 -0
- scitex/browser/collaboration/standard_interactions.py +246 -0
- scitex/browser/collaboration/visual_feedback.py +181 -0
- scitex/browser/core/BrowserMixin.py +326 -0
- scitex/browser/core/ChromeProfileManager.py +446 -0
- scitex/browser/core/__init__.py +9 -0
- scitex/browser/debugging/__init__.py +18 -0
- scitex/browser/debugging/_browser_logger.py +657 -0
- scitex/browser/debugging/_highlight_element.py +143 -0
- scitex/browser/debugging/_show_grid.py +154 -0
- scitex/browser/interaction/__init__.py +24 -0
- scitex/browser/interaction/click_center.py +149 -0
- scitex/browser/interaction/click_with_fallbacks.py +206 -0
- scitex/browser/interaction/close_popups.py +498 -0
- scitex/browser/interaction/fill_with_fallbacks.py +209 -0
- scitex/browser/pdf/__init__.py +14 -0
- scitex/browser/pdf/click_download_for_chrome_pdf_viewer.py +200 -0
- scitex/browser/pdf/detect_chrome_pdf_viewer.py +198 -0
- scitex/browser/remote/CaptchaHandler.py +434 -0
- scitex/browser/remote/ZenRowsAPIClient.py +347 -0
- scitex/browser/remote/ZenRowsBrowserManager.py +570 -0
- scitex/browser/remote/__init__.py +11 -0
- scitex/browser/stealth/HumanBehavior.py +344 -0
- scitex/browser/stealth/StealthManager.py +1008 -0
- scitex/browser/stealth/__init__.py +9 -0
- scitex/browser/template.py +122 -0
- scitex/capture/__init__.py +110 -0
- scitex/capture/__main__.py +25 -0
- scitex/capture/capture.py +848 -0
- scitex/capture/cli.py +233 -0
- scitex/capture/gif.py +344 -0
- scitex/capture/mcp_server.py +961 -0
- scitex/capture/session.py +70 -0
- scitex/capture/utils.py +705 -0
- scitex/cli/__init__.py +17 -0
- scitex/cli/cloud.py +447 -0
- scitex/cli/main.py +42 -0
- scitex/cli/scholar.py +280 -0
- scitex/context/_suppress_output.py +5 -3
- scitex/db/__init__.py +30 -3
- scitex/db/__main__.py +75 -0
- scitex/db/_check_health.py +381 -0
- scitex/db/_delete_duplicates.py +25 -386
- scitex/db/_inspect.py +335 -114
- scitex/db/_inspect_optimized.py +301 -0
- scitex/db/{_PostgreSQL.py → _postgresql/_PostgreSQL.py} +3 -3
- scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_BackupMixin.py +1 -1
- scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_BatchMixin.py +1 -1
- scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_BlobMixin.py +1 -1
- scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_ConnectionMixin.py +1 -1
- scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_MaintenanceMixin.py +1 -1
- scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_QueryMixin.py +1 -1
- scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_SchemaMixin.py +1 -1
- scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_TransactionMixin.py +1 -1
- scitex/db/_postgresql/__init__.py +6 -0
- scitex/db/_sqlite3/_SQLite3.py +210 -0
- scitex/db/_sqlite3/_SQLite3Mixins/_ArrayMixin.py +581 -0
- scitex/db/_sqlite3/_SQLite3Mixins/_ArrayMixin_v01-need-_hash-col.py +517 -0
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_BatchMixin.py +1 -1
- scitex/db/_sqlite3/_SQLite3Mixins/_BlobMixin.py +281 -0
- scitex/db/_sqlite3/_SQLite3Mixins/_ColumnMixin.py +548 -0
- scitex/db/_sqlite3/_SQLite3Mixins/_ColumnMixin_v01-indentation-issues.py +583 -0
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_ConnectionMixin.py +29 -13
- scitex/db/_sqlite3/_SQLite3Mixins/_GitMixin.py +583 -0
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_ImportExportMixin.py +1 -1
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_IndexMixin.py +1 -1
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_MaintenanceMixin.py +2 -1
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_QueryMixin.py +37 -10
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_RowMixin.py +46 -6
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_TableMixin.py +56 -10
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/_TransactionMixin.py +1 -1
- scitex/db/{_SQLite3Mixins → _sqlite3/_SQLite3Mixins}/__init__.py +14 -2
- scitex/db/_sqlite3/__init__.py +7 -0
- scitex/db/_sqlite3/_delete_duplicates.py +274 -0
- scitex/decorators/__init__.py +2 -0
- scitex/decorators/_cache_disk.py +13 -5
- scitex/decorators/_cache_disk_async.py +49 -0
- scitex/decorators/_deprecated.py +175 -10
- scitex/decorators/_timeout.py +1 -1
- scitex/dev/_analyze_code_flow.py +2 -2
- scitex/dict/_DotDict.py +73 -15
- scitex/dict/_DotDict_v01-not-handling-recursive-instantiations.py +442 -0
- scitex/dict/_DotDict_v02-not-serializing-Path-object.py +446 -0
- scitex/dict/__init__.py +2 -0
- scitex/dict/_flatten.py +27 -0
- scitex/dsp/_crop.py +2 -2
- scitex/dsp/_demo_sig.py +2 -2
- scitex/dsp/_detect_ripples.py +2 -2
- scitex/dsp/_hilbert.py +2 -2
- scitex/dsp/_listen.py +6 -6
- scitex/dsp/_modulation_index.py +2 -2
- scitex/dsp/_pac.py +1 -1
- scitex/dsp/_psd.py +2 -2
- scitex/dsp/_resample.py +2 -1
- scitex/dsp/_time.py +3 -2
- scitex/dsp/_wavelet.py +3 -2
- scitex/dsp/add_noise.py +2 -2
- scitex/dsp/example.py +1 -0
- scitex/dsp/filt.py +10 -9
- scitex/dsp/template.py +3 -2
- scitex/dsp/utils/_differential_bandpass_filters.py +1 -1
- scitex/dsp/utils/pac.py +2 -2
- scitex/dt/_normalize_timestamp.py +432 -0
- scitex/errors.py +572 -0
- scitex/gen/_DimHandler.py +2 -2
- scitex/gen/__init__.py +37 -7
- scitex/gen/_deprecated_close.py +80 -0
- scitex/gen/_deprecated_start.py +26 -0
- scitex/gen/_detect_environment.py +152 -0
- scitex/gen/_detect_notebook_path.py +169 -0
- scitex/gen/_embed.py +6 -2
- scitex/gen/_get_notebook_path.py +257 -0
- scitex/gen/_less.py +1 -1
- scitex/gen/_list_packages.py +2 -2
- scitex/gen/_norm.py +44 -9
- scitex/gen/_norm_cache.py +269 -0
- scitex/gen/_src.py +3 -5
- scitex/gen/_title_case.py +3 -3
- scitex/io/__init__.py +28 -6
- scitex/io/_glob.py +13 -7
- scitex/io/_load.py +108 -21
- scitex/io/_load_cache.py +303 -0
- scitex/io/_load_configs.py +40 -15
- scitex/io/{_H5Explorer.py → _load_modules/_H5Explorer.py} +80 -17
- scitex/io/_load_modules/_ZarrExplorer.py +114 -0
- scitex/io/_load_modules/_bibtex.py +207 -0
- scitex/io/_load_modules/_hdf5.py +53 -178
- scitex/io/_load_modules/_json.py +5 -3
- scitex/io/_load_modules/_pdf.py +871 -16
- scitex/io/_load_modules/_sqlite3.py +15 -0
- scitex/io/_load_modules/_txt.py +41 -12
- scitex/io/_load_modules/_yaml.py +4 -3
- scitex/io/_load_modules/_zarr.py +126 -0
- scitex/io/_save.py +429 -171
- scitex/io/_save_modules/__init__.py +6 -0
- scitex/io/_save_modules/_bibtex.py +194 -0
- scitex/io/_save_modules/_csv.py +8 -4
- scitex/io/_save_modules/_excel.py +174 -15
- scitex/io/_save_modules/_hdf5.py +251 -226
- scitex/io/_save_modules/_image.py +1 -3
- scitex/io/_save_modules/_json.py +49 -4
- scitex/io/_save_modules/_listed_dfs_as_csv.py +1 -3
- scitex/io/_save_modules/_listed_scalars_as_csv.py +1 -3
- scitex/io/_save_modules/_tex.py +277 -0
- scitex/io/_save_modules/_yaml.py +42 -3
- scitex/io/_save_modules/_zarr.py +160 -0
- scitex/io/utils/__init__.py +20 -0
- scitex/io/utils/h5_to_zarr.py +616 -0
- scitex/linalg/_geometric_median.py +6 -2
- scitex/{gen/_tee.py → logging/_Tee.py} +43 -84
- scitex/logging/__init__.py +122 -0
- scitex/logging/_config.py +158 -0
- scitex/logging/_context.py +103 -0
- scitex/logging/_formatters.py +128 -0
- scitex/logging/_handlers.py +64 -0
- scitex/logging/_levels.py +35 -0
- scitex/logging/_logger.py +163 -0
- scitex/logging/_print_capture.py +95 -0
- scitex/ml/__init__.py +69 -0
- scitex/{ai/genai/anthropic.py → ml/_gen_ai/_Anthropic.py} +13 -19
- scitex/{ai/genai/base_genai.py → ml/_gen_ai/_BaseGenAI.py} +5 -5
- scitex/{ai/genai/deepseek.py → ml/_gen_ai/_DeepSeek.py} +11 -16
- scitex/{ai/genai/google.py → ml/_gen_ai/_Google.py} +7 -15
- scitex/{ai/genai/groq.py → ml/_gen_ai/_Groq.py} +1 -8
- scitex/{ai/genai/llama.py → ml/_gen_ai/_Llama.py} +3 -16
- scitex/{ai/genai/openai.py → ml/_gen_ai/_OpenAI.py} +3 -3
- scitex/{ai/genai/params.py → ml/_gen_ai/_PARAMS.py} +51 -65
- scitex/{ai/genai/perplexity.py → ml/_gen_ai/_Perplexity.py} +3 -14
- scitex/ml/_gen_ai/__init__.py +43 -0
- scitex/{ai/genai/calc_cost.py → ml/_gen_ai/_calc_cost.py} +1 -1
- scitex/{ai/genai/format_output_func.py → ml/_gen_ai/_format_output_func.py} +4 -4
- scitex/{ai/genai/genai_factory.py → ml/_gen_ai/_genai_factory.py} +8 -8
- scitex/ml/activation/__init__.py +8 -0
- scitex/ml/activation/_define.py +11 -0
- scitex/{ai/classifier_server.py → ml/classification/Classifier.py} +5 -5
- scitex/ml/classification/CrossValidationExperiment.py +374 -0
- scitex/ml/classification/__init__.py +46 -0
- scitex/ml/classification/reporters/_BaseClassificationReporter.py +281 -0
- scitex/ml/classification/reporters/_ClassificationReporter.py +773 -0
- scitex/ml/classification/reporters/_MultiClassificationReporter.py +406 -0
- scitex/ml/classification/reporters/_SingleClassificationReporter.py +1834 -0
- scitex/ml/classification/reporters/__init__.py +11 -0
- scitex/ml/classification/reporters/reporter_utils/_Plotter.py +1028 -0
- scitex/ml/classification/reporters/reporter_utils/__init__.py +80 -0
- scitex/ml/classification/reporters/reporter_utils/aggregation.py +457 -0
- scitex/ml/classification/reporters/reporter_utils/data_models.py +313 -0
- scitex/ml/classification/reporters/reporter_utils/reporting.py +1056 -0
- scitex/ml/classification/reporters/reporter_utils/storage.py +221 -0
- scitex/ml/classification/reporters/reporter_utils/validation.py +395 -0
- scitex/ml/classification/timeseries/_TimeSeriesBlockingSplit.py +568 -0
- scitex/ml/classification/timeseries/_TimeSeriesCalendarSplit.py +688 -0
- scitex/ml/classification/timeseries/_TimeSeriesMetadata.py +139 -0
- scitex/ml/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +1716 -0
- scitex/ml/classification/timeseries/_TimeSeriesSlidingWindowSplit_v01-not-using-n_splits.py +1685 -0
- scitex/ml/classification/timeseries/_TimeSeriesStrategy.py +84 -0
- scitex/ml/classification/timeseries/_TimeSeriesStratifiedSplit.py +610 -0
- scitex/ml/classification/timeseries/__init__.py +39 -0
- scitex/ml/classification/timeseries/_normalize_timestamp.py +436 -0
- scitex/ml/clustering/__init__.py +11 -0
- scitex/ml/clustering/_pca.py +115 -0
- scitex/ml/clustering/_umap.py +376 -0
- scitex/ml/feature_extraction/__init__.py +56 -0
- scitex/ml/feature_extraction/vit.py +149 -0
- scitex/ml/feature_selection/__init__.py +30 -0
- scitex/ml/feature_selection/feature_selection.py +364 -0
- scitex/ml/loss/_L1L2Losses.py +34 -0
- scitex/ml/loss/__init__.py +12 -0
- scitex/ml/loss/multi_task_loss.py +47 -0
- scitex/ml/metrics/__init__.py +56 -0
- scitex/ml/metrics/_calc_bacc.py +61 -0
- scitex/ml/metrics/_calc_bacc_from_conf_mat.py +38 -0
- scitex/ml/metrics/_calc_clf_report.py +78 -0
- scitex/ml/metrics/_calc_conf_mat.py +93 -0
- scitex/ml/metrics/_calc_feature_importance.py +183 -0
- scitex/ml/metrics/_calc_mcc.py +61 -0
- scitex/ml/metrics/_calc_pre_rec_auc.py +116 -0
- scitex/ml/metrics/_calc_roc_auc.py +110 -0
- scitex/ml/metrics/_calc_seizure_prediction_metrics.py +490 -0
- scitex/ml/metrics/_calc_silhouette_score.py +503 -0
- scitex/ml/metrics/_normalize_labels.py +83 -0
- scitex/ml/optim/Ranger_Deep_Learning_Optimizer/__init__.py +0 -0
- scitex/ml/optim/Ranger_Deep_Learning_Optimizer/ranger/__init__.py +3 -0
- scitex/ml/optim/Ranger_Deep_Learning_Optimizer/ranger/ranger.py +207 -0
- scitex/ml/optim/Ranger_Deep_Learning_Optimizer/ranger/ranger2020.py +238 -0
- scitex/ml/optim/Ranger_Deep_Learning_Optimizer/ranger/ranger913A.py +215 -0
- scitex/ml/optim/Ranger_Deep_Learning_Optimizer/ranger/rangerqh.py +184 -0
- scitex/ml/optim/Ranger_Deep_Learning_Optimizer/setup.py +24 -0
- scitex/ml/optim/__init__.py +13 -0
- scitex/ml/optim/_get_set.py +31 -0
- scitex/ml/optim/_optimizers.py +71 -0
- scitex/ml/plt/__init__.py +60 -0
- scitex/ml/plt/_plot_conf_mat.py +663 -0
- scitex/ml/plt/_plot_feature_importance.py +323 -0
- scitex/ml/plt/_plot_learning_curve.py +345 -0
- scitex/ml/plt/_plot_optuna_study.py +225 -0
- scitex/ml/plt/_plot_pre_rec_curve.py +290 -0
- scitex/ml/plt/_plot_roc_curve.py +255 -0
- scitex/ml/sk/__init__.py +11 -0
- scitex/ml/sk/_clf.py +58 -0
- scitex/ml/sk/_to_sktime.py +100 -0
- scitex/ml/sklearn/__init__.py +26 -0
- scitex/ml/sklearn/clf.py +58 -0
- scitex/ml/sklearn/to_sktime.py +100 -0
- scitex/{ai/training/early_stopping.py → ml/training/_EarlyStopping.py} +1 -2
- scitex/{ai → ml/training}/_LearningCurveLogger.py +198 -242
- scitex/ml/training/__init__.py +7 -0
- scitex/ml/utils/__init__.py +22 -0
- scitex/ml/utils/_check_params.py +50 -0
- scitex/ml/utils/_default_dataset.py +46 -0
- scitex/ml/utils/_format_samples_for_sktime.py +26 -0
- scitex/ml/utils/_label_encoder.py +134 -0
- scitex/ml/utils/_merge_labels.py +22 -0
- scitex/ml/utils/_sliding_window_data_augmentation.py +11 -0
- scitex/ml/utils/_under_sample.py +51 -0
- scitex/ml/utils/_verify_n_gpus.py +16 -0
- scitex/ml/utils/grid_search.py +148 -0
- scitex/nn/_BNet.py +15 -9
- scitex/nn/_Filters.py +2 -2
- scitex/nn/_ModulationIndex.py +2 -2
- scitex/nn/_PAC.py +1 -1
- scitex/nn/_Spectrogram.py +12 -3
- scitex/nn/__init__.py +9 -10
- scitex/path/__init__.py +18 -0
- scitex/path/_clean.py +4 -0
- scitex/path/_find.py +9 -4
- scitex/path/_symlink.py +348 -0
- scitex/path/_version.py +4 -3
- scitex/pd/__init__.py +2 -0
- scitex/pd/_get_unique.py +99 -0
- scitex/plt/__init__.py +114 -5
- scitex/plt/_subplots/_AxesWrapper.py +1 -3
- scitex/plt/_subplots/_AxisWrapper.py +7 -3
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +47 -13
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +160 -2
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +26 -4
- scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +322 -0
- scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +1 -0
- scitex/plt/_subplots/_FigWrapper.py +62 -6
- scitex/plt/_subplots/_export_as_csv.py +43 -27
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +5 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +81 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_bar.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_barh.py +20 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_boxplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_contour.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_errorbar.py +35 -18
- scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_fill.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_fill_between.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_hist.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +15 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_box.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_conf_mat.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_ecdf.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_fillv.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_heatmap.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_image.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_joyplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_line.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_mean_ci.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_mean_std.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_median_iqr.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_raster.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_rectangle.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +35 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter_hist.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_shaded_line.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_violin.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_scatter.py +6 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_barplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_boxplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_heatmap.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_histplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_kdeplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_scatterplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_stripplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_swarmplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_violinplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_text.py +60 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_violin.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_violinplot.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters/test_formatters.py +1 -3
- scitex/plt/_subplots/_export_as_csv_formatters.py +56 -59
- scitex/plt/ax/_style/_hide_spines.py +1 -3
- scitex/plt/ax/_style/_rotate_labels.py +180 -76
- scitex/plt/ax/_style/_rotate_labels_v01.py +248 -0
- scitex/plt/ax/_style/_set_meta.py +11 -4
- scitex/plt/ax/_style/_set_supxyt.py +3 -3
- scitex/plt/ax/_style/_set_xyt.py +3 -3
- scitex/plt/ax/_style/_share_axes.py +2 -2
- scitex/plt/color/__init__.py +4 -4
- scitex/plt/color/{_get_colors_from_cmap.py → _get_colors_from_conf_matap.py} +7 -7
- scitex/plt/utils/_configure_mpl.py +99 -86
- scitex/plt/utils/_histogram_utils.py +1 -3
- scitex/plt/utils/_is_valid_axis.py +1 -3
- scitex/plt/utils/_scitex_config.py +1 -0
- scitex/repro/__init__.py +75 -0
- scitex/{reproduce → repro}/_gen_ID.py +1 -1
- scitex/{reproduce → repro}/_gen_timestamp.py +1 -1
- scitex/repro_rng/_RandomStateManager.py +590 -0
- scitex/repro_rng/_RandomStateManager_v01-no-verbose-options.py +414 -0
- scitex/repro_rng/__init__.py +39 -0
- scitex/reproduce/__init__.py +25 -13
- scitex/reproduce/_hash_array.py +22 -0
- scitex/resource/_get_processor_usages.py +4 -4
- scitex/resource/_get_specs.py +2 -2
- scitex/resource/_log_processor_usages.py +2 -2
- scitex/rng/_RandomStateManager.py +590 -0
- scitex/rng/_RandomStateManager_v01-no-verbose-options.py +414 -0
- scitex/rng/__init__.py +39 -0
- scitex/scholar/__init__.py +309 -19
- scitex/scholar/__main__.py +319 -0
- scitex/scholar/auth/ScholarAuthManager.py +308 -0
- scitex/scholar/auth/__init__.py +12 -0
- scitex/scholar/auth/core/AuthenticationGateway.py +473 -0
- scitex/scholar/auth/core/BrowserAuthenticator.py +386 -0
- scitex/scholar/auth/core/StrategyResolver.py +309 -0
- scitex/scholar/auth/core/__init__.py +16 -0
- scitex/scholar/auth/gateway/_OpenURLLinkFinder.py +120 -0
- scitex/scholar/auth/gateway/_OpenURLResolver.py +209 -0
- scitex/scholar/auth/gateway/__init__.py +38 -0
- scitex/scholar/auth/gateway/_resolve_functions.py +101 -0
- scitex/scholar/auth/providers/BaseAuthenticator.py +166 -0
- scitex/scholar/auth/providers/EZProxyAuthenticator.py +484 -0
- scitex/scholar/auth/providers/OpenAthensAuthenticator.py +619 -0
- scitex/scholar/auth/providers/ShibbolethAuthenticator.py +686 -0
- scitex/scholar/auth/providers/__init__.py +18 -0
- scitex/scholar/auth/session/AuthCacheManager.py +189 -0
- scitex/scholar/auth/session/SessionManager.py +159 -0
- scitex/scholar/auth/session/__init__.py +11 -0
- scitex/scholar/auth/sso/BaseSSOAutomator.py +373 -0
- scitex/scholar/auth/sso/OpenAthensSSOAutomator.py +378 -0
- scitex/scholar/auth/sso/SSOAutomator.py +180 -0
- scitex/scholar/auth/sso/UniversityOfMelbourneSSOAutomator.py +380 -0
- scitex/scholar/auth/sso/__init__.py +15 -0
- scitex/scholar/browser/ScholarBrowserManager.py +705 -0
- scitex/scholar/browser/__init__.py +38 -0
- scitex/scholar/browser/utils/__init__.py +13 -0
- scitex/scholar/browser/utils/click_and_wait.py +205 -0
- scitex/scholar/browser/utils/close_unwanted_pages.py +140 -0
- scitex/scholar/browser/utils/wait_redirects.py +732 -0
- scitex/scholar/config/PublisherRules.py +132 -0
- scitex/scholar/config/ScholarConfig.py +126 -0
- scitex/scholar/config/__init__.py +17 -0
- scitex/scholar/core/Paper.py +627 -0
- scitex/scholar/core/Papers.py +722 -0
- scitex/scholar/core/Scholar.py +1975 -0
- scitex/scholar/core/__init__.py +9 -0
- scitex/scholar/impact_factor/ImpactFactorEngine.py +204 -0
- scitex/scholar/impact_factor/__init__.py +20 -0
- scitex/scholar/impact_factor/estimation/ImpactFactorEstimationEngine.py +0 -0
- scitex/scholar/impact_factor/estimation/__init__.py +40 -0
- scitex/scholar/impact_factor/estimation/build_database.py +0 -0
- scitex/scholar/impact_factor/estimation/core/__init__.py +28 -0
- scitex/scholar/impact_factor/estimation/core/cache_manager.py +523 -0
- scitex/scholar/impact_factor/estimation/core/calculator.py +355 -0
- scitex/scholar/impact_factor/estimation/core/journal_matcher.py +428 -0
- scitex/scholar/integration/__init__.py +59 -0
- scitex/scholar/integration/base.py +502 -0
- scitex/scholar/integration/mendeley/__init__.py +22 -0
- scitex/scholar/integration/mendeley/exporter.py +166 -0
- scitex/scholar/integration/mendeley/importer.py +236 -0
- scitex/scholar/integration/mendeley/linker.py +79 -0
- scitex/scholar/integration/mendeley/mapper.py +212 -0
- scitex/scholar/integration/zotero/__init__.py +27 -0
- scitex/scholar/integration/zotero/__main__.py +264 -0
- scitex/scholar/integration/zotero/exporter.py +351 -0
- scitex/scholar/integration/zotero/importer.py +372 -0
- scitex/scholar/integration/zotero/linker.py +415 -0
- scitex/scholar/integration/zotero/mapper.py +286 -0
- scitex/scholar/metadata_engines/ScholarEngine.py +588 -0
- scitex/scholar/metadata_engines/__init__.py +21 -0
- scitex/scholar/metadata_engines/individual/ArXivEngine.py +397 -0
- scitex/scholar/metadata_engines/individual/CrossRefEngine.py +274 -0
- scitex/scholar/metadata_engines/individual/CrossRefLocalEngine.py +263 -0
- scitex/scholar/metadata_engines/individual/OpenAlexEngine.py +350 -0
- scitex/scholar/metadata_engines/individual/PubMedEngine.py +329 -0
- scitex/scholar/metadata_engines/individual/SemanticScholarEngine.py +438 -0
- scitex/scholar/metadata_engines/individual/URLDOIEngine.py +410 -0
- scitex/scholar/metadata_engines/individual/_BaseDOIEngine.py +487 -0
- scitex/scholar/metadata_engines/individual/__init__.py +7 -0
- scitex/scholar/metadata_engines/utils/_PubMedConverter.py +469 -0
- scitex/scholar/metadata_engines/utils/_URLDOIExtractor.py +283 -0
- scitex/scholar/metadata_engines/utils/__init__.py +30 -0
- scitex/scholar/metadata_engines/utils/_metadata2bibtex.py +103 -0
- scitex/scholar/metadata_engines/utils/_standardize_metadata.py +376 -0
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +579 -0
- scitex/scholar/pdf_download/__init__.py +5 -0
- scitex/scholar/pdf_download/strategies/__init__.py +38 -0
- scitex/scholar/pdf_download/strategies/chrome_pdf_viewer.py +376 -0
- scitex/scholar/pdf_download/strategies/direct_download.py +131 -0
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +167 -0
- scitex/scholar/pdf_download/strategies/manual_download_utils.py +996 -0
- scitex/scholar/pdf_download/strategies/response_body.py +207 -0
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +364 -0
- scitex/scholar/pipelines/ScholarPipelineParallel.py +478 -0
- scitex/scholar/pipelines/ScholarPipelineSingle.py +767 -0
- scitex/scholar/pipelines/__init__.py +49 -0
- scitex/scholar/storage/BibTeXHandler.py +1018 -0
- scitex/scholar/storage/PaperIO.py +468 -0
- scitex/scholar/storage/ScholarLibrary.py +182 -0
- scitex/scholar/storage/_DeduplicationManager.py +548 -0
- scitex/scholar/storage/_LibraryCacheManager.py +724 -0
- scitex/scholar/storage/_LibraryManager.py +1835 -0
- scitex/scholar/storage/__init__.py +28 -0
- scitex/scholar/url_finder/ScholarURLFinder.py +379 -0
- scitex/scholar/url_finder/__init__.py +7 -0
- scitex/scholar/url_finder/strategies/__init__.py +33 -0
- scitex/scholar/url_finder/strategies/find_pdf_urls_by_direct_links.py +261 -0
- scitex/scholar/url_finder/strategies/find_pdf_urls_by_dropdown.py +67 -0
- scitex/scholar/url_finder/strategies/find_pdf_urls_by_href.py +204 -0
- scitex/scholar/url_finder/strategies/find_pdf_urls_by_navigation.py +256 -0
- scitex/scholar/url_finder/strategies/find_pdf_urls_by_publisher_patterns.py +165 -0
- scitex/scholar/url_finder/strategies/find_pdf_urls_by_zotero_translators.py +163 -0
- scitex/scholar/url_finder/strategies/find_supplementary_urls_by_href.py +70 -0
- scitex/scholar/utils/__init__.py +22 -0
- scitex/scholar/utils/bibtex/__init__.py +9 -0
- scitex/scholar/utils/bibtex/_parse_bibtex.py +71 -0
- scitex/scholar/utils/cleanup/__init__.py +8 -0
- scitex/scholar/utils/cleanup/_cleanup_scholar_processes.py +96 -0
- scitex/scholar/utils/cleanup/cleanup_old_extractions.py +117 -0
- scitex/scholar/utils/text/_TextNormalizer.py +407 -0
- scitex/scholar/utils/text/__init__.py +9 -0
- scitex/scholar/zotero/__init__.py +38 -0
- scitex/session/__init__.py +51 -0
- scitex/session/_lifecycle.py +736 -0
- scitex/session/_manager.py +102 -0
- scitex/session/template.py +122 -0
- scitex/stats/__init__.py +30 -26
- scitex/stats/correct/__init__.py +21 -0
- scitex/stats/correct/_correct_bonferroni.py +551 -0
- scitex/stats/correct/_correct_fdr.py +634 -0
- scitex/stats/correct/_correct_holm.py +548 -0
- scitex/stats/correct/_correct_sidak.py +499 -0
- scitex/stats/descriptive/__init__.py +85 -0
- scitex/stats/descriptive/_circular.py +540 -0
- scitex/stats/descriptive/_describe.py +219 -0
- scitex/stats/descriptive/_nan.py +518 -0
- scitex/stats/descriptive/_real.py +189 -0
- scitex/stats/effect_sizes/__init__.py +41 -0
- scitex/stats/effect_sizes/_cliffs_delta.py +325 -0
- scitex/stats/effect_sizes/_cohens_d.py +342 -0
- scitex/stats/effect_sizes/_epsilon_squared.py +315 -0
- scitex/stats/effect_sizes/_eta_squared.py +302 -0
- scitex/stats/effect_sizes/_prob_superiority.py +296 -0
- scitex/stats/posthoc/__init__.py +19 -0
- scitex/stats/posthoc/_dunnett.py +463 -0
- scitex/stats/posthoc/_games_howell.py +383 -0
- scitex/stats/posthoc/_tukey_hsd.py +367 -0
- scitex/stats/power/__init__.py +19 -0
- scitex/stats/power/_power.py +433 -0
- scitex/stats/template.py +119 -0
- scitex/stats/utils/__init__.py +62 -0
- scitex/stats/utils/_effect_size.py +985 -0
- scitex/stats/utils/_formatters.py +270 -0
- scitex/stats/utils/_normalizers.py +927 -0
- scitex/stats/utils/_power.py +433 -0
- scitex/stats_v01/_EffectSizeCalculator.py +488 -0
- scitex/stats_v01/_StatisticalValidator.py +411 -0
- scitex/stats_v01/__init__.py +60 -0
- scitex/stats_v01/_additional_tests.py +415 -0
- scitex/{stats → stats_v01}/_p2stars.py +19 -5
- scitex/stats_v01/_two_sample_tests.py +141 -0
- scitex/stats_v01/desc/__init__.py +83 -0
- scitex/stats_v01/desc/_circular.py +540 -0
- scitex/stats_v01/desc/_describe.py +219 -0
- scitex/stats_v01/desc/_nan.py +518 -0
- scitex/{stats/desc/_nan.py → stats_v01/desc/_nan_v01-20250920_145731.py} +23 -12
- scitex/stats_v01/desc/_real.py +189 -0
- scitex/stats_v01/tests/__corr_test_optimized.py +221 -0
- scitex/stats_v01/tests/_corr_test_optimized.py +179 -0
- scitex/str/__init__.py +1 -3
- scitex/str/_clean_path.py +6 -2
- scitex/str/_latex_fallback.py +267 -160
- scitex/str/_parse.py +44 -36
- scitex/str/_printc.py +1 -3
- scitex/template/__init__.py +87 -0
- scitex/template/_create_project.py +267 -0
- scitex/template/create_pip_project.py +80 -0
- scitex/template/create_research.py +80 -0
- scitex/template/create_singularity.py +80 -0
- scitex/units.py +291 -0
- scitex/utils/_compress_hdf5.py +14 -3
- scitex/utils/_email.py +21 -2
- scitex/utils/_grid.py +6 -4
- scitex/utils/_notify.py +13 -10
- scitex/utils/_verify_scitex_format.py +589 -0
- scitex/utils/_verify_scitex_format_v01.py +370 -0
- scitex/utils/template.py +122 -0
- scitex/web/_search_pubmed.py +62 -16
- scitex-2.1.0.dist-info/LICENSE +21 -0
- scitex-2.1.0.dist-info/METADATA +677 -0
- scitex-2.1.0.dist-info/RECORD +919 -0
- {scitex-2.0.0.dist-info → scitex-2.1.0.dist-info}/WHEEL +1 -1
- scitex-2.1.0.dist-info/entry_points.txt +3 -0
- scitex/ai/__Classifiers.py +0 -101
- scitex/ai/classification/classification_reporter.py +0 -1137
- scitex/ai/classification/classifiers.py +0 -101
- scitex/ai/classification_reporter.py +0 -1161
- scitex/ai/genai/__init__.py +0 -277
- scitex/ai/genai/anthropic_provider.py +0 -320
- scitex/ai/genai/anthropic_refactored.py +0 -109
- scitex/ai/genai/auth_manager.py +0 -200
- scitex/ai/genai/base_provider.py +0 -291
- scitex/ai/genai/chat_history.py +0 -307
- scitex/ai/genai/cost_tracker.py +0 -276
- scitex/ai/genai/deepseek_provider.py +0 -251
- scitex/ai/genai/google_provider.py +0 -228
- scitex/ai/genai/groq_provider.py +0 -248
- scitex/ai/genai/image_processor.py +0 -250
- scitex/ai/genai/llama_provider.py +0 -214
- scitex/ai/genai/mock_provider.py +0 -127
- scitex/ai/genai/model_registry.py +0 -304
- scitex/ai/genai/openai_provider.py +0 -293
- scitex/ai/genai/perplexity_provider.py +0 -205
- scitex/ai/genai/provider_base.py +0 -302
- scitex/ai/genai/provider_factory.py +0 -370
- scitex/ai/genai/response_handler.py +0 -235
- scitex/ai/layer/_Pass.py +0 -21
- scitex/ai/layer/__init__.py +0 -10
- scitex/ai/layer/_switch.py +0 -8
- scitex/ai/metrics/_bACC.py +0 -51
- scitex/ai/plt/_learning_curve.py +0 -194
- scitex/ai/plt/_optuna_study.py +0 -111
- scitex/ai/plt/aucs/__init__.py +0 -2
- scitex/ai/plt/aucs/example.py +0 -60
- scitex/ai/plt/aucs/pre_rec_auc.py +0 -223
- scitex/ai/plt/aucs/roc_auc.py +0 -246
- scitex/ai/sampling/undersample.py +0 -29
- scitex/db/_SQLite3.py +0 -2136
- scitex/db/_SQLite3Mixins/_BlobMixin.py +0 -229
- scitex/gen/_close.py +0 -222
- scitex/gen/_start.py +0 -451
- scitex/general/__init__.py +0 -5
- scitex/io/_load_modules/_db.py +0 -24
- scitex/life/__init__.py +0 -10
- scitex/life/_monitor_rain.py +0 -49
- scitex/reproduce/_fix_seeds.py +0 -45
- scitex/res/__init__.py +0 -5
- scitex/scholar/_local_search.py +0 -454
- scitex/scholar/_paper.py +0 -244
- scitex/scholar/_pdf_downloader.py +0 -325
- scitex/scholar/_search.py +0 -393
- scitex/scholar/_vector_search.py +0 -370
- scitex/scholar/_web_sources.py +0 -457
- scitex/stats/desc/__init__.py +0 -40
- scitex-2.0.0.dist-info/METADATA +0 -307
- scitex-2.0.0.dist-info/RECORD +0 -572
- scitex-2.0.0.dist-info/licenses/LICENSE +0 -7
- /scitex/ai/{act → activation}/__init__.py +0 -0
- /scitex/ai/{act → activation}/_define.py +0 -0
- /scitex/ai/{early_stopping.py → training/_EarlyStopping.py} +0 -0
- /scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_ImportExportMixin.py +0 -0
- /scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_IndexMixin.py +0 -0
- /scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_RowMixin.py +0 -0
- /scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/_TableMixin.py +0 -0
- /scitex/db/{_PostgreSQLMixins → _postgresql/_PostgreSQLMixins}/__init__.py +0 -0
- /scitex/{stats → stats_v01}/_calc_partial_corr.py +0 -0
- /scitex/{stats → stats_v01}/_corr_test_multi.py +0 -0
- /scitex/{stats → stats_v01}/_corr_test_wrapper.py +0 -0
- /scitex/{stats → stats_v01}/_describe_wrapper.py +0 -0
- /scitex/{stats → stats_v01}/_multiple_corrections.py +0 -0
- /scitex/{stats → stats_v01}/_nan_stats.py +0 -0
- /scitex/{stats → stats_v01}/_p2stars_wrapper.py +0 -0
- /scitex/{stats → stats_v01}/_statistical_tests.py +0 -0
- /scitex/{stats/desc/_describe.py → stats_v01/desc/_describe_v01-20250920_145731.py} +0 -0
- /scitex/{stats/desc/_real.py → stats_v01/desc/_real_v01-20250920_145731.py} +0 -0
- /scitex/{stats → stats_v01}/multiple/__init__.py +0 -0
- /scitex/{stats → stats_v01}/multiple/_bonferroni_correction.py +0 -0
- /scitex/{stats → stats_v01}/multiple/_fdr_correction.py +0 -0
- /scitex/{stats → stats_v01}/multiple/_multicompair.py +0 -0
- /scitex/{stats → stats_v01}/tests/__corr_test.py +0 -0
- /scitex/{stats → stats_v01}/tests/__corr_test_multi.py +0 -0
- /scitex/{stats → stats_v01}/tests/__corr_test_single.py +0 -0
- /scitex/{stats → stats_v01}/tests/__init__.py +0 -0
- /scitex/{stats → stats_v01}/tests/_brunner_munzel_test.py +0 -0
- /scitex/{stats → stats_v01}/tests/_nocorrelation_test.py +0 -0
- /scitex/{stats → stats_v01}/tests/_smirnov_grubbs.py +0 -0
- {scitex-2.0.0.dist-info → scitex-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-10-11 01:05:03 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex_repo/src/scitex/scholar/auth/core/BrowserAuthenticator.py
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import os
|
|
8
|
+
__FILE__ = (
|
|
9
|
+
"./src/scitex/scholar/auth/core/BrowserAuthenticator.py"
|
|
10
|
+
)
|
|
11
|
+
__DIR__ = os.path.dirname(__FILE__)
|
|
12
|
+
# ----------------------------------------
|
|
13
|
+
|
|
14
|
+
"""Browser-based authentication operations.
|
|
15
|
+
|
|
16
|
+
This module handles browser interactions for authentication,
|
|
17
|
+
including login detection, navigation, and session verification.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from playwright.async_api import Page, async_playwright
|
|
24
|
+
|
|
25
|
+
from scitex import logging
|
|
26
|
+
from scitex.browser.core import BrowserMixin
|
|
27
|
+
from scitex.browser.interaction import (
|
|
28
|
+
click_with_fallbacks_async,
|
|
29
|
+
fill_with_fallbacks_async,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BrowserAuthenticator(BrowserMixin):
|
|
36
|
+
"""Handles browser-based authentication operations."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self, mode: str = "interactive", timeout: int = 300, sso_automator=None
|
|
40
|
+
):
|
|
41
|
+
"""Initialize browser authenticator.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
mode: Browser mode - 'interactive' for authentication, 'stealth' for scraping
|
|
45
|
+
timeout: Timeout for browser operations in seconds
|
|
46
|
+
sso_automator: Optional SSO automator instance for institution-specific handling
|
|
47
|
+
"""
|
|
48
|
+
super().__init__(mode=mode)
|
|
49
|
+
self.name = self.__class__.__name__
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self.sso_automator = sso_automator
|
|
52
|
+
|
|
53
|
+
async def navigate_to_login_async(self, url: str) -> Page:
|
|
54
|
+
"""Navigate to login URL and return page.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
url: Login URL to navigate to
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Page object for further operations
|
|
61
|
+
"""
|
|
62
|
+
# Use the BrowserMixin's get_browser_async method to get properly configured browser
|
|
63
|
+
browser = await self.get_browser_async()
|
|
64
|
+
|
|
65
|
+
# Create context with proper interactive viewport settings
|
|
66
|
+
context_options = {}
|
|
67
|
+
if self.mode == "interactive":
|
|
68
|
+
context_options["viewport"] = {"width": 1280, "height": 720}
|
|
69
|
+
else:
|
|
70
|
+
context_options["viewport"] = {"width": 1, "height": 1}
|
|
71
|
+
|
|
72
|
+
context = await browser.new_context(**context_options)
|
|
73
|
+
await context.add_init_script(
|
|
74
|
+
self.cookie_acceptor.get_auto_acceptor_script()
|
|
75
|
+
)
|
|
76
|
+
# await self.cookie_acceptor.inject_auto_acceptor_async(context)
|
|
77
|
+
page = await context.new_page()
|
|
78
|
+
|
|
79
|
+
logger.info(f"{self.name}: Navigating to: {url}")
|
|
80
|
+
await page.goto(url, wait_until="domcontentloaded")
|
|
81
|
+
|
|
82
|
+
# Check for cookie banner and warn if present
|
|
83
|
+
if await self.cookie_acceptor.check_cookie_banner_exists_async(page):
|
|
84
|
+
logger.warning(
|
|
85
|
+
"{self.name}: Cookie banner detected - may need manual acceptance"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return page
|
|
89
|
+
|
|
90
|
+
async def wait_for_login_completion_async(
|
|
91
|
+
self, page: Page, success_indicators: List[str]
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""Wait for login completion by monitoring URL changes and handling SSO automation.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
page: Browser page to monitor
|
|
97
|
+
success_indicators: List of URL patterns indicating successful login
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if login successful, False if timed out
|
|
101
|
+
"""
|
|
102
|
+
max_wait_time = self.timeout
|
|
103
|
+
check_interval = 2
|
|
104
|
+
elapsed_time = 0
|
|
105
|
+
seen_sso_page = False
|
|
106
|
+
openathens_automated = False
|
|
107
|
+
|
|
108
|
+
while elapsed_time < max_wait_time:
|
|
109
|
+
current_url = page.url
|
|
110
|
+
|
|
111
|
+
# Track SSO navigation
|
|
112
|
+
if self._is_sso_page(current_url):
|
|
113
|
+
seen_sso_page = True
|
|
114
|
+
logger.debug(
|
|
115
|
+
f"{self.name}: Detected SSO/login page: {current_url}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Handle different authentication flows based on current URL
|
|
119
|
+
automation_attempted = False
|
|
120
|
+
|
|
121
|
+
# Priority 1: OpenAthens page automation (if on OpenAthens and not yet automated)
|
|
122
|
+
if not openathens_automated and self._is_openathens_page(
|
|
123
|
+
current_url
|
|
124
|
+
):
|
|
125
|
+
logger.debug(
|
|
126
|
+
f"{self.name}: OpenAthens page detected - attempting automation"
|
|
127
|
+
)
|
|
128
|
+
automation_attempted = True
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
from ..sso.OpenAthensSSOAutomator import (
|
|
132
|
+
OpenAthensSSOAutomator,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
openathens_automator = OpenAthensSSOAutomator()
|
|
136
|
+
|
|
137
|
+
if openathens_automator.is_sso_page(current_url):
|
|
138
|
+
oa_success = await openathens_automator.handle_sso_redirect_async(
|
|
139
|
+
page
|
|
140
|
+
)
|
|
141
|
+
if oa_success:
|
|
142
|
+
logger.info(
|
|
143
|
+
f"{self.name}: OpenAthens page automation completed"
|
|
144
|
+
)
|
|
145
|
+
openathens_automated = True
|
|
146
|
+
else:
|
|
147
|
+
logger.warning(
|
|
148
|
+
f"{self.name}: OpenAthens page automation failed"
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.warning(
|
|
152
|
+
f"{self.name}: OpenAthens automation failed: {e}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Priority 2: Institution-specific SSO automation (if available and on SSO page)
|
|
156
|
+
elif self.sso_automator and self.sso_automator.is_sso_page(
|
|
157
|
+
current_url
|
|
158
|
+
):
|
|
159
|
+
institution_name = self.sso_automator.get_institution_name()
|
|
160
|
+
logger.info(
|
|
161
|
+
f"{self.name}: {institution_name} SSO detected - attempting automation"
|
|
162
|
+
)
|
|
163
|
+
automation_attempted = True
|
|
164
|
+
|
|
165
|
+
sso_success = (
|
|
166
|
+
await self.sso_automator.handle_sso_redirect_async(page)
|
|
167
|
+
)
|
|
168
|
+
if sso_success:
|
|
169
|
+
logger.info(
|
|
170
|
+
f"{self.name}: {institution_name} SSO automation completed"
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
logger.warning(
|
|
174
|
+
f"{self.name}: {institution_name} SSO automation failed"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Priority 3: Generic automation attempt for unknown SSO pages
|
|
178
|
+
elif self._is_sso_page(current_url) and not automation_attempted:
|
|
179
|
+
logger.info(
|
|
180
|
+
f"{self.name}: Generic SSO page detected - attempting basic automation"
|
|
181
|
+
)
|
|
182
|
+
automation_attempted = True
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
generic_success = (
|
|
186
|
+
await self._attempt_generic_sso_automation(page)
|
|
187
|
+
)
|
|
188
|
+
if generic_success:
|
|
189
|
+
logger.info(
|
|
190
|
+
f"{self.name}: Generic SSO automation completed"
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
logger.info(
|
|
194
|
+
f"{self.name}: Generic SSO automation not applicable"
|
|
195
|
+
)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.debug(
|
|
198
|
+
f"{self.name}: Generic SSO automation failed: {e}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# If automation was attempted but we're still on the same page,
|
|
202
|
+
# it likely failed and requires manual intervention
|
|
203
|
+
if automation_attempted and elapsed_time > 30:
|
|
204
|
+
if elapsed_time % 30 == 0: # Show message every 30 seconds
|
|
205
|
+
logger.info(
|
|
206
|
+
f"{self.name}: Automation completed - waiting for manual completion if needed"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Check for success
|
|
210
|
+
if self._check_success_indicators(current_url, success_indicators):
|
|
211
|
+
if await self._verify_login_success_async(
|
|
212
|
+
page, seen_sso_page, elapsed_time
|
|
213
|
+
):
|
|
214
|
+
logger.info(
|
|
215
|
+
f"{self.name}: Login successful detected at URL: {current_url}"
|
|
216
|
+
)
|
|
217
|
+
logger.info(
|
|
218
|
+
f"{self.name}: Login detected! Capturing session..."
|
|
219
|
+
)
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
# Show progress
|
|
223
|
+
if elapsed_time % 10 == 0 and elapsed_time > 0:
|
|
224
|
+
logger.info(
|
|
225
|
+
f"{self.name}: Waiting for login... ({elapsed_time}s elapsed)"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
await asyncio.sleep(check_interval)
|
|
229
|
+
elapsed_time += check_interval
|
|
230
|
+
|
|
231
|
+
logger.error(f"{self.name}: Login timeout - please try again")
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
async def verify_authentication_async(
|
|
235
|
+
self, verification_url: str, cookies: List[Dict[str, Any]]
|
|
236
|
+
) -> bool:
|
|
237
|
+
"""Verify authentication by checking access to protected page.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
verification_url: URL to test access to
|
|
241
|
+
cookies: Authentication cookies to use
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if authentication verified, False otherwise
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
# Use stealth mode for verification (minimal viewport)
|
|
248
|
+
original_mode = self.mode
|
|
249
|
+
self.mode = "stealth"
|
|
250
|
+
|
|
251
|
+
async with async_playwright() as p:
|
|
252
|
+
browser, context = await self.create_browser_context_async(p)
|
|
253
|
+
|
|
254
|
+
# Add cookies
|
|
255
|
+
if cookies:
|
|
256
|
+
await context.add_cookies(cookies)
|
|
257
|
+
|
|
258
|
+
page = await context.new_page()
|
|
259
|
+
|
|
260
|
+
# Navigate to verification URL
|
|
261
|
+
response = await page.goto(
|
|
262
|
+
verification_url,
|
|
263
|
+
wait_until="domcontentloaded",
|
|
264
|
+
timeout=15000,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
current_url = page.url
|
|
268
|
+
is_authenticate_async = self._check_authenticate_async_page(
|
|
269
|
+
current_url
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
await browser.close()
|
|
273
|
+
|
|
274
|
+
if is_authenticate_async:
|
|
275
|
+
logger.info(
|
|
276
|
+
f"{self.name}: Verified live authentication at {current_url}"
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
logger.debug(
|
|
280
|
+
f"{self.name}: Authentication verification failed at {current_url}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return is_authenticate_async
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.error(
|
|
287
|
+
f"{self.name}: Authentication verification failed: {e}"
|
|
288
|
+
)
|
|
289
|
+
return False
|
|
290
|
+
finally:
|
|
291
|
+
# Restore original mode setting
|
|
292
|
+
self.mode = original_mode
|
|
293
|
+
|
|
294
|
+
async def extract_session_cookies_async(
|
|
295
|
+
self, page: Page
|
|
296
|
+
) -> tuple[Dict[str, str], List[Dict[str, Any]]]:
|
|
297
|
+
"""Extract session cookies from browser page.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
page: Browser page to extract cookies from
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Tuple of (simple_cookies_dict, full_cookies_list)
|
|
304
|
+
"""
|
|
305
|
+
cookies = await page.context.cookies()
|
|
306
|
+
simple_cookies = {c["name"]: c["value"] for c in cookies}
|
|
307
|
+
return simple_cookies, cookies
|
|
308
|
+
|
|
309
|
+
async def reliable_click_async(self, page: Page, selector: str) -> bool:
|
|
310
|
+
"""Perform reliable click using shared utility."""
|
|
311
|
+
return await click_with_fallbacks_async(page, selector)
|
|
312
|
+
|
|
313
|
+
async def reliable_fill_async(
|
|
314
|
+
self, page: Page, selector: str, value: str
|
|
315
|
+
) -> bool:
|
|
316
|
+
"""Perform reliable form fill using shared utility."""
|
|
317
|
+
return await fill_with_fallbacks_async(page, selector, value)
|
|
318
|
+
|
|
319
|
+
def display_login_instructions(
|
|
320
|
+
self, email: Optional[str], timeout: int
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Display login instructions to user using proper logging.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
email: User email to display
|
|
326
|
+
timeout: Timeout to display
|
|
327
|
+
"""
|
|
328
|
+
logger.info(f"{self.name}: OpenAthens Authentication Required")
|
|
329
|
+
logger.info(f"{self.name}: MyAthens login page is opening...")
|
|
330
|
+
if email:
|
|
331
|
+
logger.info(f"{self.name}: Account: {email}")
|
|
332
|
+
logger.info(f"{self.name}: Please complete the login process:")
|
|
333
|
+
logger.info(f"{self.name}: 1. Enter your institutional email")
|
|
334
|
+
logger.info(f"{self.name}: 2. Click your institution when it appears")
|
|
335
|
+
logger.info(
|
|
336
|
+
f"{self.name}: 3. Complete login on your institution's page"
|
|
337
|
+
)
|
|
338
|
+
logger.info(
|
|
339
|
+
f"{self.name}: 4. You'll be redirected back to OpenAthens when done"
|
|
340
|
+
)
|
|
341
|
+
logger.info(f"{self.name}: 5. Timeout is {timeout} seconds")
|
|
342
|
+
logger.info(f"{self.name}: 6. Close the window after successful login")
|
|
343
|
+
|
|
344
|
+
# Show environment variables
|
|
345
|
+
logger.debug(f"{self.name}: OpenAthens Environment Variables:")
|
|
346
|
+
for key, value in os.environ.items():
|
|
347
|
+
if "SCITEX_SCHOLAR_OPENATHENS" in key:
|
|
348
|
+
logger.debug(f"{self.name}: {key}: {value}")
|
|
349
|
+
|
|
350
|
+
def _is_sso_page(self, url: str) -> bool:
|
|
351
|
+
"""Check if URL indicates SSO/login page."""
|
|
352
|
+
return "sso" in url.lower() or "login" in url.lower()
|
|
353
|
+
|
|
354
|
+
def _is_openathens_page(self, url: str) -> bool:
|
|
355
|
+
"""Check if URL is OpenAthens page."""
|
|
356
|
+
return (
|
|
357
|
+
"my.openathens.net" in url.lower()
|
|
358
|
+
or "openathens.net" in url.lower()
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _check_success_indicators(
|
|
362
|
+
self, url: str, indicators: List[str]
|
|
363
|
+
) -> bool:
|
|
364
|
+
"""Check if URL matches any success indicators."""
|
|
365
|
+
return any(indicator in url for indicator in indicators)
|
|
366
|
+
|
|
367
|
+
async def _verify_login_success_async(
|
|
368
|
+
self, page: Page, seen_sso_page: bool, elapsed_time: int
|
|
369
|
+
) -> bool:
|
|
370
|
+
"""Verify login success with additional checks."""
|
|
371
|
+
# Additional verification logic can be added here
|
|
372
|
+
return seen_sso_page or elapsed_time > 30
|
|
373
|
+
|
|
374
|
+
def _check_authenticate_async_page(self, url: str) -> bool:
|
|
375
|
+
"""Check if URL indicates authenticate_async page."""
|
|
376
|
+
authenticate_async_patterns = ["/account", "/app", "/library"]
|
|
377
|
+
unauthenticate_async_patterns = ["login", "signin"]
|
|
378
|
+
|
|
379
|
+
# Check for unauthenticate_async patterns first
|
|
380
|
+
if any(pattern in url for pattern in unauthenticate_async_patterns):
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
# Check for authenticate_async patterns
|
|
384
|
+
return any(pattern in url for pattern in authenticate_async_patterns)
|
|
385
|
+
|
|
386
|
+
# EOF
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-10-10 00:37:47 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex_repo/src/scitex/scholar/auth/_AuthenticationStrategyResolver.py
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import os
|
|
8
|
+
__FILE__ = (
|
|
9
|
+
"./src/scitex/scholar/auth/core/StrategyResolver.py"
|
|
10
|
+
)
|
|
11
|
+
__DIR__ = os.path.dirname(__FILE__)
|
|
12
|
+
# ----------------------------------------
|
|
13
|
+
|
|
14
|
+
"""Authentication strategy resolver that determines the best approach based on user information."""
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from scitex import logging
|
|
21
|
+
from scitex.scholar.config import ScholarConfig
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AuthenticationMethod(Enum):
|
|
27
|
+
"""Available authentication methods."""
|
|
28
|
+
|
|
29
|
+
DIRECT_SSO = "direct_sso" # Direct to institution SSO
|
|
30
|
+
OPENATHENS_ONLY = "openathens_only" # OpenAthens without SSO redirect
|
|
31
|
+
OPENATHENS_TO_SSO = "openathens_sso" # OpenAthens that redirects to SSO
|
|
32
|
+
MANUAL = "manual" # Manual intervention required
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AuthenticationStrategy:
|
|
37
|
+
"""Authentication strategy configuration."""
|
|
38
|
+
|
|
39
|
+
method: AuthenticationMethod
|
|
40
|
+
primary_url: str
|
|
41
|
+
openathens_email: Optional[str] = None
|
|
42
|
+
sso_automator_available: bool = False
|
|
43
|
+
institution_name: Optional[str] = None
|
|
44
|
+
confidence: float = 0.0 # 0.0 to 1.0
|
|
45
|
+
fallback_methods: List[AuthenticationMethod] = None
|
|
46
|
+
|
|
47
|
+
def __post_init__(self):
|
|
48
|
+
if self.fallback_methods is None:
|
|
49
|
+
self.fallback_methods = [AuthenticationMethod.MANUAL]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AuthenticationStrategyResolver:
|
|
53
|
+
"""Resolves the best authentication strategy based on user information."""
|
|
54
|
+
|
|
55
|
+
# Known institution configurations
|
|
56
|
+
INSTITUTION_CONFIGS = {
|
|
57
|
+
# University of Melbourne
|
|
58
|
+
"unimelb.edu.au": {
|
|
59
|
+
"name": "University of Melbourne",
|
|
60
|
+
"openathens_supported": True,
|
|
61
|
+
"direct_sso_url": "https://sso.unimelb.edu.au/",
|
|
62
|
+
"openathens_redirects_to_sso": True,
|
|
63
|
+
"sso_automator": "UniversityOfMelbourne",
|
|
64
|
+
},
|
|
65
|
+
# Add more institutions as needed
|
|
66
|
+
"stanford.edu": {
|
|
67
|
+
"name": "Stanford University",
|
|
68
|
+
"openathens_supported": True,
|
|
69
|
+
"direct_sso_url": "https://login.stanford.edu/",
|
|
70
|
+
"openathens_redirects_to_sso": True,
|
|
71
|
+
"sso_automator": None,
|
|
72
|
+
},
|
|
73
|
+
"mit.edu": {
|
|
74
|
+
"name": "MIT",
|
|
75
|
+
"openathens_supported": False,
|
|
76
|
+
"direct_sso_url": "https://oidc.mit.edu/",
|
|
77
|
+
"openathens_redirects_to_sso": False,
|
|
78
|
+
"sso_automator": None,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def __init__(self, config: Optional[ScholarConfig] = None):
|
|
83
|
+
"""Initialize resolver with configuration."""
|
|
84
|
+
self.name = self.__class__.__name__
|
|
85
|
+
self.config = config or ScholarConfig()
|
|
86
|
+
|
|
87
|
+
def resolve_strategy(
|
|
88
|
+
self,
|
|
89
|
+
openathens_email: Optional[str] = None,
|
|
90
|
+
target_url: Optional[str] = None,
|
|
91
|
+
institution_name: Optional[str] = None,
|
|
92
|
+
prefer_openathens: bool = True,
|
|
93
|
+
) -> AuthenticationStrategy:
|
|
94
|
+
"""Resolve the best authentication strategy.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
openathens_email: User's institutional email
|
|
98
|
+
target_url: Target URL being accessed
|
|
99
|
+
institution_name: Explicit institution name
|
|
100
|
+
prefer_openathens: Whether to prefer OpenAthens when available
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
AuthenticationStrategy with the recommended approach
|
|
104
|
+
"""
|
|
105
|
+
logger.info(f"{self.name}: Resolving authentication strategy...")
|
|
106
|
+
|
|
107
|
+
# Get email from config if not provided
|
|
108
|
+
if not openathens_email:
|
|
109
|
+
openathens_email = self.config.resolve(
|
|
110
|
+
"openathens_email", None, None, str
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if not openathens_email:
|
|
114
|
+
logger.warning(
|
|
115
|
+
f"{self.name}: No email provided - defaulting to manual authentication"
|
|
116
|
+
)
|
|
117
|
+
return AuthenticationStrategy(
|
|
118
|
+
method=AuthenticationMethod.MANUAL,
|
|
119
|
+
primary_url=target_url
|
|
120
|
+
or "https://my.openathens.net/?passiveLogin=false",
|
|
121
|
+
confidence=0.0,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Extract domain from email
|
|
125
|
+
email_domain = self._extract_domain(openathens_email)
|
|
126
|
+
institution_config = self.INSTITUTION_CONFIGS.get(email_domain)
|
|
127
|
+
|
|
128
|
+
if institution_config:
|
|
129
|
+
return self._resolve_known_institution_strategy(
|
|
130
|
+
openathens_email,
|
|
131
|
+
email_domain,
|
|
132
|
+
institution_config,
|
|
133
|
+
target_url,
|
|
134
|
+
prefer_openathens,
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
return self._resolve_unknown_institution_strategy(
|
|
138
|
+
openathens_email, email_domain, target_url, prefer_openathens
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _extract_domain(self, email: str) -> str:
|
|
142
|
+
"""Extract domain from email address."""
|
|
143
|
+
if "@" not in email:
|
|
144
|
+
return email.lower()
|
|
145
|
+
return email.split("@")[1].lower()
|
|
146
|
+
|
|
147
|
+
def _resolve_known_institution_strategy(
|
|
148
|
+
self,
|
|
149
|
+
openathens_email: str,
|
|
150
|
+
email_domain: str,
|
|
151
|
+
institution_config: Dict[str, Any],
|
|
152
|
+
target_url: Optional[str],
|
|
153
|
+
prefer_openathens: bool,
|
|
154
|
+
) -> AuthenticationStrategy:
|
|
155
|
+
"""Resolve strategy for known institution."""
|
|
156
|
+
|
|
157
|
+
institution_name = institution_config["name"]
|
|
158
|
+
sso_automator_available = (
|
|
159
|
+
institution_config.get("sso_automator") is not None
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
logger.info(
|
|
163
|
+
f"{self.name}: Resolving strategy for known institution: {institution_name}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Determine primary method based on preferences and capabilities
|
|
167
|
+
if prefer_openathens and institution_config.get(
|
|
168
|
+
"openathens_supported", False
|
|
169
|
+
):
|
|
170
|
+
if institution_config.get("openathens_redirects_to_sso", False):
|
|
171
|
+
# OpenAthens → SSO flow
|
|
172
|
+
method = AuthenticationMethod.OPENATHENS_TO_SSO
|
|
173
|
+
primary_url = "https://my.openathens.net/?passiveLogin=false"
|
|
174
|
+
confidence = 0.9 if sso_automator_available else 0.7
|
|
175
|
+
fallback_methods = [
|
|
176
|
+
AuthenticationMethod.DIRECT_SSO,
|
|
177
|
+
AuthenticationMethod.MANUAL,
|
|
178
|
+
]
|
|
179
|
+
else:
|
|
180
|
+
# OpenAthens only
|
|
181
|
+
method = AuthenticationMethod.OPENATHENS_ONLY
|
|
182
|
+
primary_url = "https://my.openathens.net/?passiveLogin=false"
|
|
183
|
+
confidence = 0.8
|
|
184
|
+
fallback_methods = [
|
|
185
|
+
AuthenticationMethod.DIRECT_SSO,
|
|
186
|
+
AuthenticationMethod.MANUAL,
|
|
187
|
+
]
|
|
188
|
+
else:
|
|
189
|
+
# Direct SSO
|
|
190
|
+
method = AuthenticationMethod.DIRECT_SSO
|
|
191
|
+
primary_url = institution_config.get(
|
|
192
|
+
"direct_sso_url", target_url or ""
|
|
193
|
+
)
|
|
194
|
+
confidence = 0.8 if sso_automator_available else 0.5
|
|
195
|
+
fallback_methods = (
|
|
196
|
+
[
|
|
197
|
+
AuthenticationMethod.OPENATHENS_TO_SSO,
|
|
198
|
+
AuthenticationMethod.MANUAL,
|
|
199
|
+
]
|
|
200
|
+
if institution_config.get("openathens_supported", False)
|
|
201
|
+
else [AuthenticationMethod.MANUAL]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return AuthenticationStrategy(
|
|
205
|
+
method=method,
|
|
206
|
+
primary_url=primary_url,
|
|
207
|
+
openathens_email=openathens_email,
|
|
208
|
+
sso_automator_available=sso_automator_available,
|
|
209
|
+
institution_name=institution_name,
|
|
210
|
+
confidence=confidence,
|
|
211
|
+
fallback_methods=fallback_methods,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _resolve_unknown_institution_strategy(
|
|
215
|
+
self,
|
|
216
|
+
openathens_email: str,
|
|
217
|
+
email_domain: str,
|
|
218
|
+
target_url: Optional[str],
|
|
219
|
+
prefer_openathens: bool,
|
|
220
|
+
) -> AuthenticationStrategy:
|
|
221
|
+
"""Resolve strategy for unknown institution."""
|
|
222
|
+
|
|
223
|
+
logger.info(
|
|
224
|
+
f"{self.name}: Resolving strategy for unknown institution: {email_domain}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# For unknown institutions, try OpenAthens first as it's most generic
|
|
228
|
+
if prefer_openathens:
|
|
229
|
+
method = AuthenticationMethod.OPENATHENS_TO_SSO
|
|
230
|
+
primary_url = "https://my.openathens.net/?passiveLogin=false"
|
|
231
|
+
confidence = 0.6 # Lower confidence for unknown institutions
|
|
232
|
+
fallback_methods = [AuthenticationMethod.MANUAL]
|
|
233
|
+
else:
|
|
234
|
+
method = AuthenticationMethod.MANUAL
|
|
235
|
+
primary_url = (
|
|
236
|
+
target_url or "https://my.openathens.net/?passiveLogin=false"
|
|
237
|
+
)
|
|
238
|
+
confidence = 0.3
|
|
239
|
+
fallback_methods = []
|
|
240
|
+
|
|
241
|
+
return AuthenticationStrategy(
|
|
242
|
+
method=method,
|
|
243
|
+
primary_url=primary_url,
|
|
244
|
+
openathens_email=openathens_email,
|
|
245
|
+
sso_automator_available=False,
|
|
246
|
+
institution_name=f"Unknown ({email_domain})",
|
|
247
|
+
confidence=confidence,
|
|
248
|
+
fallback_methods=fallback_methods,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def get_supported_institutions(self) -> List[str]:
|
|
252
|
+
"""Get list of supported institutions."""
|
|
253
|
+
return [config["name"] for config in self.INSTITUTION_CONFIGS.values()]
|
|
254
|
+
|
|
255
|
+
def add_institution_config(
|
|
256
|
+
self,
|
|
257
|
+
domain: str,
|
|
258
|
+
name: str,
|
|
259
|
+
openathens_supported: bool = True,
|
|
260
|
+
direct_sso_url: Optional[str] = None,
|
|
261
|
+
openathens_redirects_to_sso: bool = True,
|
|
262
|
+
sso_automator: Optional[str] = None,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Add configuration for a new institution."""
|
|
265
|
+
self.INSTITUTION_CONFIGS[domain.lower()] = {
|
|
266
|
+
"name": name,
|
|
267
|
+
"openathens_supported": openathens_supported,
|
|
268
|
+
"direct_sso_url": direct_sso_url,
|
|
269
|
+
"openathens_redirects_to_sso": openathens_redirects_to_sso,
|
|
270
|
+
"sso_automator": sso_automator,
|
|
271
|
+
}
|
|
272
|
+
logger.info(
|
|
273
|
+
f"{self.name}: Added institution configuration: {name} ({domain})"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
|
|
279
|
+
def main():
|
|
280
|
+
"""Test the authentication strategy resolver."""
|
|
281
|
+
resolver = AuthenticationStrategyResolver()
|
|
282
|
+
|
|
283
|
+
# Test known institution
|
|
284
|
+
strategy = resolver.resolve_strategy(
|
|
285
|
+
openathens_email="test@unimelb.edu.au"
|
|
286
|
+
)
|
|
287
|
+
print(
|
|
288
|
+
f"UniMelb Strategy: {strategy.method.value}, URL: {strategy.primary_url}, Confidence: {strategy.confidence}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Test unknown institution
|
|
292
|
+
strategy = resolver.resolve_strategy(
|
|
293
|
+
openathens_email="test@unknown.edu"
|
|
294
|
+
)
|
|
295
|
+
print(
|
|
296
|
+
f"Unknown Strategy: {strategy.method.value}, URL: {strategy.primary_url}, Confidence: {strategy.confidence}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Test manual fallback
|
|
300
|
+
strategy = resolver.resolve_strategy()
|
|
301
|
+
print(
|
|
302
|
+
f"No Email Strategy: {strategy.method.value}, URL: {strategy.primary_url}, Confidence: {strategy.confidence}"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
main()
|
|
306
|
+
|
|
307
|
+
# python -m scitex.scholar.auth._AuthenticationStrategyResolver
|
|
308
|
+
|
|
309
|
+
# EOF
|