simstack 2.0.1__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.
- simstack/__init__.py +0 -0
- simstack/_version.py +24 -0
- simstack/core/__init__.py +0 -0
- simstack/core/artifacts.py +406 -0
- simstack/core/asnyc_helper.py +14 -0
- simstack/core/config_file.py +60 -0
- simstack/core/context.py +283 -0
- simstack/core/definitions.py +22 -0
- simstack/core/engine.py +115 -0
- simstack/core/find_simstack_modules.py +41 -0
- simstack/core/hash.py +179 -0
- simstack/core/node.py +1137 -0
- simstack/core/node_claim.py +31 -0
- simstack/core/node_runner.py +362 -0
- simstack/core/process_results.py +102 -0
- simstack/core/recompute_artifacts.py +117 -0
- simstack/core/reset_db.py +12 -0
- simstack/core/resource_assignment.py +222 -0
- simstack/core/resources.py +90 -0
- simstack/core/route_table.py +24 -0
- simstack/core/run_docker.py +109 -0
- simstack/core/run_node.py +71 -0
- simstack/core/runner.py +169 -0
- simstack/core/services/__init__.py +11 -0
- simstack/core/services/base_service.py +191 -0
- simstack/core/services/file_transfer_service.py +117 -0
- simstack/core/services/git_restart_service.py +12 -0
- simstack/core/services/git_uv_update_service.py +105 -0
- simstack/core/services/node_execution_service.py +182 -0
- simstack/core/services/resource_branch_monitor_service.py +71 -0
- simstack/core/services/runner_cleanup_service.py +149 -0
- simstack/core/services/runner_manager.py +163 -0
- simstack/core/services/runner_status_service.py +10 -0
- simstack/core/services/slurm_status_service.py +67 -0
- simstack/core/services/stop_check_service.py +27 -0
- simstack/core/services/timeout_restart_service.py +26 -0
- simstack/core/simstack_result.py +39 -0
- simstack/core/submit_node.py +217 -0
- simstack/core/task_id.py +22 -0
- simstack/methods/__init__.py +0 -0
- simstack/methods/archive_files.py +292 -0
- simstack/methods/fire_and_forget_runner.py +113 -0
- simstack/methods/generate_test.py +125 -0
- simstack/methods/get_file.py +26 -0
- simstack/methods/mass_runner.py +154 -0
- simstack/methods/multiple_executor.py +307 -0
- simstack/methods/switch_git.py +16 -0
- simstack/methods/upload_helpers.py +81 -0
- simstack/models/__init__.py +74 -0
- simstack/models/array_list.py +21 -0
- simstack/models/array_storage.py +83 -0
- simstack/models/artifact_models.py +127 -0
- simstack/models/base_lists.py +51 -0
- simstack/models/base_types.py +209 -0
- simstack/models/charts_artifact.py +690 -0
- simstack/models/dataset.py +451 -0
- simstack/models/dataset_metadata.py +390 -0
- simstack/models/datasettuple.py +587 -0
- simstack/models/file_instance.py +150 -0
- simstack/models/file_list.py +45 -0
- simstack/models/files.py +424 -0
- simstack/models/fire_and_forget_result.py +19 -0
- simstack/models/images2d.py +113 -0
- simstack/models/log_entry_model.py +37 -0
- simstack/models/models.py +52 -0
- simstack/models/named_data_reference.py +36 -0
- simstack/models/node_registry.py +96 -0
- simstack/models/pandas_model.py +239 -0
- simstack/models/parameters.py +402 -0
- simstack/models/pickle_models.py +300 -0
- simstack/models/project.py +13 -0
- simstack/models/resource_assignment.py +170 -0
- simstack/models/resource_definition.py +125 -0
- simstack/models/runner_model.py +39 -0
- simstack/models/simple_table.py +79 -0
- simstack/models/simstack_model.py +97 -0
- simstack/models/slurm_info.py +17 -0
- simstack/models/table_artifact.py +87 -0
- simstack/models/tag.py +12 -0
- simstack/models/unused_test_file.py +5 -0
- simstack/simstack_marker.com +1 -0
- simstack/static/docs/.buildinfo +4 -0
- simstack/static/docs/_images/GUI Concept.webp +0 -0
- simstack/static/docs/_modules/index.html +434 -0
- simstack/static/docs/_modules/simstack/core/artifacts.html +743 -0
- simstack/static/docs/_modules/simstack/core/asnyc_helper.html +330 -0
- simstack/static/docs/_modules/simstack/core/config_file.html +379 -0
- simstack/static/docs/_modules/simstack/core/definitions.html +341 -0
- simstack/static/docs/_modules/simstack/core/engine.html +437 -0
- simstack/static/docs/_modules/simstack/core/find_simstack_modules.html +360 -0
- simstack/static/docs/_modules/simstack/core/hash.html +528 -0
- simstack/static/docs/_modules/simstack/core/node.html +1392 -0
- simstack/static/docs/_modules/simstack/core/node_claim.html +347 -0
- simstack/static/docs/_modules/simstack/core/node_runner.html +711 -0
- simstack/static/docs/_modules/simstack/core/process_results.html +418 -0
- simstack/static/docs/_modules/simstack/core/recompute_artifacts.html +433 -0
- simstack/static/docs/_modules/simstack/core/reset_db.html +328 -0
- simstack/static/docs/_modules/simstack/core/resource_assignment.html +553 -0
- simstack/static/docs/_modules/simstack/core/resources.html +424 -0
- simstack/static/docs/_modules/simstack/core/route_table.html +349 -0
- simstack/static/docs/_modules/simstack/core/run_docker.html +425 -0
- simstack/static/docs/_modules/simstack/core/run_node.html +390 -0
- simstack/static/docs/_modules/simstack/core/runner.html +491 -0
- simstack/static/docs/_modules/simstack/core/services/base_service.html +522 -0
- simstack/static/docs/_modules/simstack/core/services/file_transfer_service.html +436 -0
- simstack/static/docs/_modules/simstack/core/services/git_restart_service.html +331 -0
- simstack/static/docs/_modules/simstack/core/services/git_uv_update_service.html +427 -0
- simstack/static/docs/_modules/simstack/core/services/node_execution_service.html +507 -0
- simstack/static/docs/_modules/simstack/core/services/resource_branch_monitor_service.html +390 -0
- simstack/static/docs/_modules/simstack/core/services/runner_cleanup_service.html +468 -0
- simstack/static/docs/_modules/simstack/core/services/runner_manager.html +485 -0
- simstack/static/docs/_modules/simstack/core/services/runner_status_service.html +329 -0
- simstack/static/docs/_modules/simstack/core/services/slurm_status_service.html +386 -0
- simstack/static/docs/_modules/simstack/core/services/stop_check_service.html +346 -0
- simstack/static/docs/_modules/simstack/core/services/timeout_restart_service.html +345 -0
- simstack/static/docs/_modules/simstack/core/simstack_result.html +355 -0
- simstack/static/docs/_modules/simstack/core/submit_node.html +536 -0
- simstack/static/docs/_modules/simstack/core/task_id.html +344 -0
- simstack/static/docs/_modules/simstack/methods/archive_files.html +598 -0
- simstack/static/docs/_modules/simstack/methods/fire_and_forget_runner.html +432 -0
- simstack/static/docs/_modules/simstack/methods/generate_test.html +443 -0
- simstack/static/docs/_modules/simstack/methods/get_file.html +342 -0
- simstack/static/docs/_modules/simstack/methods/mass_runner.html +476 -0
- simstack/static/docs/_modules/simstack/methods/multiple_executor.html +674 -0
- simstack/static/docs/_modules/simstack/methods/switch_git.html +332 -0
- simstack/static/docs/_modules/simstack/methods/upload_helpers.html +403 -0
- simstack/static/docs/_modules/simstack/models/array_list.html +337 -0
- simstack/static/docs/_modules/simstack/models/array_storage.html +417 -0
- simstack/static/docs/_modules/simstack/models/base_lists.html +374 -0
- simstack/static/docs/_modules/simstack/models/base_types.html +588 -0
- simstack/static/docs/_modules/simstack/models/charts_artifact.html +1035 -0
- simstack/static/docs/_modules/simstack/models/dataset.html +866 -0
- simstack/static/docs/_modules/simstack/models/dataset_metadata.html +711 -0
- simstack/static/docs/_modules/simstack/models/datasettuple.html +1005 -0
- simstack/static/docs/_modules/simstack/models/file_instance.html +472 -0
- simstack/static/docs/_modules/simstack/models/file_list.html +367 -0
- simstack/static/docs/_modules/simstack/models/files.html +773 -0
- simstack/static/docs/_modules/simstack/models/fire_and_forget_result.html +335 -0
- simstack/static/docs/_modules/simstack/models/images2d.html +447 -0
- simstack/static/docs/_modules/simstack/models/log_entry_model.html +356 -0
- simstack/static/docs/_modules/simstack/models/models.html +374 -0
- simstack/static/docs/_modules/simstack/models/named_data_reference.html +355 -0
- simstack/static/docs/_modules/simstack/models/node_registry.html +415 -0
- simstack/static/docs/_modules/simstack/models/pandas_model.html +570 -0
- simstack/static/docs/_modules/simstack/models/parameters.html +739 -0
- simstack/static/docs/_modules/simstack/models/pickle_models.html +631 -0
- simstack/static/docs/_modules/simstack/models/project.html +329 -0
- simstack/static/docs/_modules/simstack/models/resource_assignment.html +501 -0
- simstack/static/docs/_modules/simstack/models/resource_definition.html +477 -0
- simstack/static/docs/_modules/simstack/models/runner_model.html +361 -0
- simstack/static/docs/_modules/simstack/models/simple_table.html +413 -0
- simstack/static/docs/_modules/simstack/models/simstack_model.html +416 -0
- simstack/static/docs/_modules/simstack/models/slurm_info.html +333 -0
- simstack/static/docs/_modules/simstack/models/table_artifact.html +406 -0
- simstack/static/docs/_modules/simstack/models/tag.html +328 -0
- simstack/static/docs/_modules/simstack/tables/model_table.html +519 -0
- simstack/static/docs/_modules/simstack/tables/node_children.html +538 -0
- simstack/static/docs/_modules/simstack/tables/node_table.html +854 -0
- simstack/static/docs/_modules/simstack/tables/table_builder.html +642 -0
- simstack/static/docs/_modules/simstack/util/async_zip_utils.html +337 -0
- simstack/static/docs/_modules/simstack/util/b64mixin.html +339 -0
- simstack/static/docs/_modules/simstack/util/cleaned_json_schema.html +336 -0
- simstack/static/docs/_modules/simstack/util/config_reader.html +498 -0
- simstack/static/docs/_modules/simstack/util/custom_model_dump.html +391 -0
- simstack/static/docs/_modules/simstack/util/database_information.html +488 -0
- simstack/static/docs/_modules/simstack/util/db.html +1392 -0
- simstack/static/docs/_modules/simstack/util/db_logger.html +448 -0
- simstack/static/docs/_modules/simstack/util/default_from_dict.html +376 -0
- simstack/static/docs/_modules/simstack/util/directory_iterator.html +553 -0
- simstack/static/docs/_modules/simstack/util/docstring_parser.html +526 -0
- simstack/static/docs/_modules/simstack/util/file_hashing.html +394 -0
- simstack/static/docs/_modules/simstack/util/file_transfer_client.html +791 -0
- simstack/static/docs/_modules/simstack/util/files.html +733 -0
- simstack/static/docs/_modules/simstack/util/find_python_files.html +349 -0
- simstack/static/docs/_modules/simstack/util/generate_ui_schema.html +396 -0
- simstack/static/docs/_modules/simstack/util/generic_list_mixin.html +487 -0
- simstack/static/docs/_modules/simstack/util/get_module_path.html +336 -0
- simstack/static/docs/_modules/simstack/util/git_pull.html +381 -0
- simstack/static/docs/_modules/simstack/util/git_repository_status.html +411 -0
- simstack/static/docs/_modules/simstack/util/import_module.html +377 -0
- simstack/static/docs/_modules/simstack/util/importer.html +637 -0
- simstack/static/docs/_modules/simstack/util/init_data_source.html +357 -0
- simstack/static/docs/_modules/simstack/util/make_graph.html +423 -0
- simstack/static/docs/_modules/simstack/util/make_table.html +1171 -0
- simstack/static/docs/_modules/simstack/util/mappings.html +378 -0
- simstack/static/docs/_modules/simstack/util/minimal_route_finder.html +388 -0
- simstack/static/docs/_modules/simstack/util/mock_db.html +472 -0
- simstack/static/docs/_modules/simstack/util/module_path_checker.html +354 -0
- simstack/static/docs/_modules/simstack/util/object_list_mixin.html +649 -0
- simstack/static/docs/_modules/simstack/util/path_manager.html +566 -0
- simstack/static/docs/_modules/simstack/util/project_root_finder.html +357 -0
- simstack/static/docs/_modules/simstack/util/queue_watcher.html +593 -0
- simstack/static/docs/_modules/simstack/util/resource_config.html +532 -0
- simstack/static/docs/_modules/simstack/util/route_finder.html +390 -0
- simstack/static/docs/_modules/simstack/util/runner_util.html +487 -0
- simstack/static/docs/_modules/simstack/util/runner_utils.html +468 -0
- simstack/static/docs/_modules/simstack/util/safe_code_executor.html +510 -0
- simstack/static/docs/_modules/simstack/util/sample_config.html +414 -0
- simstack/static/docs/_modules/simstack/util/sensitive_content_filter.html +364 -0
- simstack/static/docs/_modules/simstack/util/setup_logging.html +365 -0
- simstack/static/docs/_modules/simstack/util/submit_to_watchdog.html +425 -0
- simstack/static/docs/_modules/simstack/util/toml_reader.html +482 -0
- simstack/static/docs/_modules/simstack/util/transform_file_name.html +357 -0
- simstack/static/docs/_modules/simstack/util/ui_tools.html +541 -0
- simstack/static/docs/_sources/architecture.rst.txt +25 -0
- simstack/static/docs/_sources/array_lists.rst.txt +60 -0
- simstack/static/docs/_sources/ci_pipeline.rst.txt +34 -0
- simstack/static/docs/_sources/data.rst.txt +175 -0
- simstack/static/docs/_sources/database_migrations.rst.txt +310 -0
- simstack/static/docs/_sources/datasets.rst.txt +93 -0
- simstack/static/docs/_sources/developer_guide.rst.txt +25 -0
- simstack/static/docs/_sources/docker_nodes.rst.txt +149 -0
- simstack/static/docs/_sources/files.rst.txt +159 -0
- simstack/static/docs/_sources/fire_and_forget_runner.rst.txt +67 -0
- simstack/static/docs/_sources/index.rst.txt +25 -0
- simstack/static/docs/_sources/installation.rst.txt +213 -0
- simstack/static/docs/_sources/lists.rst.txt +111 -0
- simstack/static/docs/_sources/mass_runner.rst.txt +91 -0
- simstack/static/docs/_sources/misc.rst.txt +106 -0
- simstack/static/docs/_sources/overview.rst.txt +91 -0
- simstack/static/docs/_sources/recovery.rst.txt +38 -0
- simstack/static/docs/_sources/resource_config.rst.txt +141 -0
- simstack/static/docs/_sources/simstack/modules/modules.rst.txt +7 -0
- simstack/static/docs/_sources/simstack/modules/simstack.core.rst.txt +205 -0
- simstack/static/docs/_sources/simstack/modules/simstack.core.services.rst.txt +109 -0
- simstack/static/docs/_sources/simstack/modules/simstack.methods.rst.txt +77 -0
- simstack/static/docs/_sources/simstack/modules/simstack.models.rst.txt +261 -0
- simstack/static/docs/_sources/simstack/modules/simstack.rst.txt +22 -0
- simstack/static/docs/_sources/simstack/modules/simstack.tables.rst.txt +45 -0
- simstack/static/docs/_sources/simstack/modules/simstack.util.rst.txt +381 -0
- simstack/static/docs/_sources/submit.rst.txt +141 -0
- simstack/static/docs/_sources/tables.rst.txt +50 -0
- simstack/static/docs/_sources/testing_complex_nodes.rst.txt +92 -0
- simstack/static/docs/_sources/ui_documentation.rst.txt +272 -0
- simstack/static/docs/_sources/workflows.rst.txt +446 -0
- simstack/static/docs/_static/base-stemmer.js +476 -0
- simstack/static/docs/_static/basic.css +906 -0
- simstack/static/docs/_static/cobalt2-theme.css +1 -0
- simstack/static/docs/_static/debug.css +69 -0
- simstack/static/docs/_static/docs-build-badge.css +51 -0
- simstack/static/docs/_static/docs-build-badge.js +51 -0
- simstack/static/docs/_static/doctools.js +150 -0
- simstack/static/docs/_static/documentation_options.js +13 -0
- simstack/static/docs/_static/english-stemmer.js +1066 -0
- simstack/static/docs/_static/file.png +0 -0
- simstack/static/docs/_static/language_data.js +13 -0
- simstack/static/docs/_static/minus.png +0 -0
- simstack/static/docs/_static/plus.png +0 -0
- simstack/static/docs/_static/pygments.css +250 -0
- simstack/static/docs/_static/scripts/furo-extensions.js +0 -0
- simstack/static/docs/_static/scripts/furo.js +3 -0
- simstack/static/docs/_static/scripts/furo.js.LICENSE.txt +7 -0
- simstack/static/docs/_static/scripts/furo.js.map +1 -0
- simstack/static/docs/_static/searchtools.js +693 -0
- simstack/static/docs/_static/skeleton.css +296 -0
- simstack/static/docs/_static/sphinx_highlight.js +159 -0
- simstack/static/docs/_static/styles/furo-extensions.css +2 -0
- simstack/static/docs/_static/styles/furo-extensions.css.map +1 -0
- simstack/static/docs/_static/styles/furo.css +2 -0
- simstack/static/docs/_static/styles/furo.css.map +1 -0
- simstack/static/docs/architecture.html +340 -0
- simstack/static/docs/array_lists.html +678 -0
- simstack/static/docs/ci_pipeline.html +385 -0
- simstack/static/docs/data.html +545 -0
- simstack/static/docs/database_migrations.html +652 -0
- simstack/static/docs/datasets.html +1268 -0
- simstack/static/docs/developer_guide.html +359 -0
- simstack/static/docs/docker_nodes.html +503 -0
- simstack/static/docs/files.html +718 -0
- simstack/static/docs/fire_and_forget_runner.html +641 -0
- simstack/static/docs/genindex.html +6390 -0
- simstack/static/docs/index.html +516 -0
- simstack/static/docs/installation.html +553 -0
- simstack/static/docs/lists.html +478 -0
- simstack/static/docs/mass_runner.html +481 -0
- simstack/static/docs/misc.html +454 -0
- simstack/static/docs/objects.inv +0 -0
- simstack/static/docs/overview.html +469 -0
- simstack/static/docs/py-modindex.html +1111 -0
- simstack/static/docs/recovery.html +371 -0
- simstack/static/docs/resource_config.html +590 -0
- simstack/static/docs/search.html +327 -0
- simstack/static/docs/searchindex.js +1 -0
- simstack/static/docs/simstack/modules/modules.html +489 -0
- simstack/static/docs/simstack/modules/simstack.core.html +1883 -0
- simstack/static/docs/simstack/modules/simstack.core.services.html +698 -0
- simstack/static/docs/simstack/modules/simstack.html +3060 -0
- simstack/static/docs/simstack/modules/simstack.methods.html +1150 -0
- simstack/static/docs/simstack/modules/simstack.models.html +13113 -0
- simstack/static/docs/simstack/modules/simstack.tables.html +635 -0
- simstack/static/docs/simstack/modules/simstack.util.html +3320 -0
- simstack/static/docs/submit.html +487 -0
- simstack/static/docs/tables.html +539 -0
- simstack/static/docs/testing_complex_nodes.html +458 -0
- simstack/static/docs/ui_documentation.html +788 -0
- simstack/static/docs/workflows.html +766 -0
- simstack/tables/__init__.py +0 -0
- simstack/tables/model_table.py +191 -0
- simstack/tables/node_children.py +219 -0
- simstack/tables/node_table.py +517 -0
- simstack/tables/table_builder.py +314 -0
- simstack/util/__init__.py +0 -0
- simstack/util/async_zip_utils.py +18 -0
- simstack/util/b64mixin.py +23 -0
- simstack/util/cleaned_json_schema.py +20 -0
- simstack/util/config_reader.py +179 -0
- simstack/util/custom_model_dump.py +75 -0
- simstack/util/database_information.py +160 -0
- simstack/util/db.py +1013 -0
- simstack/util/db_logger.py +117 -0
- simstack/util/default_from_dict.py +57 -0
- simstack/util/directory_iterator.py +216 -0
- simstack/util/docstring_parser.py +186 -0
- simstack/util/file_hashing.py +72 -0
- simstack/util/file_transfer_client.py +427 -0
- simstack/util/files.py +396 -0
- simstack/util/find_python_files.py +33 -0
- simstack/util/generate_ui_schema.py +80 -0
- simstack/util/generic_list_mixin.py +120 -0
- simstack/util/geometry_utils.py +118 -0
- simstack/util/get_module_path.py +20 -0
- simstack/util/git_pull.py +65 -0
- simstack/util/git_repository_status.py +95 -0
- simstack/util/import_module.py +63 -0
- simstack/util/importer.py +312 -0
- simstack/util/init_data_source.py +41 -0
- simstack/util/make_graph.py +107 -0
- simstack/util/make_table.py +837 -0
- simstack/util/mappings.py +41 -0
- simstack/util/minimal_route_finder.py +72 -0
- simstack/util/mock_db.py +144 -0
- simstack/util/module_path_checker.py +38 -0
- simstack/util/object_list_mixin.py +282 -0
- simstack/util/path_manager.py +223 -0
- simstack/util/project_root_finder.py +41 -0
- simstack/util/queue_watcher.py +262 -0
- simstack/util/resource_config.py +198 -0
- simstack/util/route_finder.py +74 -0
- simstack/util/runner_util.py +166 -0
- simstack/util/runner_utils.py +143 -0
- simstack/util/safe_code_executor.py +194 -0
- simstack/util/sample_config.py +98 -0
- simstack/util/sensitive_content_filter.py +48 -0
- simstack/util/setup_logging.py +49 -0
- simstack/util/submit_to_watchdog.py +106 -0
- simstack/util/toml_reader.py +142 -0
- simstack/util/transform_file_name.py +41 -0
- simstack/util/ui_tools.py +213 -0
- simstack-2.0.1.dist-info/METADATA +454 -0
- simstack-2.0.1.dist-info/RECORD +354 -0
- simstack-2.0.1.dist-info/WHEEL +5 -0
- simstack-2.0.1.dist-info/entry_points.txt +12 -0
- simstack-2.0.1.dist-info/licenses/LICENSE +21 -0
- simstack-2.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Dict, Optional, Iterator, Any, Union
|
|
3
|
+
|
|
4
|
+
from simstack.util.directory_iterator import DirectoryPath
|
|
5
|
+
from simstack.util.project_root_finder import find_project_root
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PathManager:
|
|
9
|
+
"""
|
|
10
|
+
Manages paths for the SimStack application, providing mechanisms to find Python files
|
|
11
|
+
for nodes and models. Will read only .py files.
|
|
12
|
+
By default travers all directories below the project root.
|
|
13
|
+
Uses DirectoryPath for efficient directory traversal.
|
|
14
|
+
"""
|
|
15
|
+
_instance = None
|
|
16
|
+
|
|
17
|
+
def __new__(cls, *args, **kwargs):
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
cls._instance = super(PathManager, cls).__new__(cls)
|
|
20
|
+
cls._instance._initialized = False
|
|
21
|
+
return cls._instance
|
|
22
|
+
|
|
23
|
+
def default_excludes(self):
|
|
24
|
+
return [
|
|
25
|
+
"__pycache__",
|
|
26
|
+
"*.pyc",
|
|
27
|
+
".git",
|
|
28
|
+
".venv",
|
|
29
|
+
"venv",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
def reset(self):
|
|
33
|
+
self._initialized = False
|
|
34
|
+
self.paths.clear()
|
|
35
|
+
self._excluded_patterns = self.default_excludes()
|
|
36
|
+
|
|
37
|
+
def __init__(self, use_pickle: bool = False, include_project_root: bool = False):
|
|
38
|
+
"""
|
|
39
|
+
Initialize the PathManager.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
use_pickle: Boolean flag indicating whether to use pickle for serialization
|
|
43
|
+
"""
|
|
44
|
+
if self._initialized:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
self._initialized = True
|
|
48
|
+
self.use_pickle = use_pickle
|
|
49
|
+
self.root_dir = find_project_root()
|
|
50
|
+
self.paths: Dict[str, Dict[str, Union[Path, str, bool]]] = {}
|
|
51
|
+
|
|
52
|
+
if include_project_root:
|
|
53
|
+
self.add_path("project_root", self.root_dir)
|
|
54
|
+
|
|
55
|
+
self._excluded_patterns = self.default_excludes()
|
|
56
|
+
|
|
57
|
+
def add_path(self, name: str, path: Path, drops: str = "", use_pickle: bool = False) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Add a path to the manager.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
name: Name identifier for the path
|
|
63
|
+
path: The directory path relative to the project root
|
|
64
|
+
drops: Prefix to drop from module names (for import paths)
|
|
65
|
+
use_pickle: Whether to use pickle for this path
|
|
66
|
+
"""
|
|
67
|
+
# Convert relative paths to absolute paths
|
|
68
|
+
if not path.is_absolute():
|
|
69
|
+
path = self.root_dir / path
|
|
70
|
+
|
|
71
|
+
if not path.is_dir():
|
|
72
|
+
raise ValueError(f"'{path}' is not a valid directory")
|
|
73
|
+
|
|
74
|
+
self.paths[name] = {"path": path, "drops": drops, "use_pickle": use_pickle}
|
|
75
|
+
|
|
76
|
+
def get_path(self, name: str) -> Dict[str, str]:
|
|
77
|
+
"""
|
|
78
|
+
Get a path by name.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
name: Name of the path to retrieve
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dictionary containing path information
|
|
85
|
+
"""
|
|
86
|
+
if name not in self.paths:
|
|
87
|
+
raise KeyError(f"Path '{name}' not found in PathManager")
|
|
88
|
+
|
|
89
|
+
return self.paths[name]
|
|
90
|
+
|
|
91
|
+
def find_python_files(self, path_name: str, excluded_patterns: Optional[List[str]] = None) -> List[str]:
|
|
92
|
+
"""
|
|
93
|
+
Find Python files in the specified path, excluding __init__.py files.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
path_name: Name of the path to search in
|
|
97
|
+
excluded_patterns: Additional patterns to exclude
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of absolute file paths to Python files
|
|
101
|
+
"""
|
|
102
|
+
path_info = self.get_path(path_name)
|
|
103
|
+
path = path_info["path"]
|
|
104
|
+
|
|
105
|
+
# Combine default and additional exclusion patterns
|
|
106
|
+
all_excluded_patterns = self._excluded_patterns.copy()
|
|
107
|
+
if excluded_patterns:
|
|
108
|
+
all_excluded_patterns.extend(excluded_patterns)
|
|
109
|
+
|
|
110
|
+
# Add __init__.py to excluded patterns
|
|
111
|
+
all_excluded_patterns.append("__init__.py")
|
|
112
|
+
|
|
113
|
+
# Use DirectoryPath to find Python files
|
|
114
|
+
dir_path = DirectoryPath(path, all_excluded_patterns, [".py"])
|
|
115
|
+
|
|
116
|
+
# Convert Path objects to strings for compatibility with existing code
|
|
117
|
+
return [str(file_path) for file_path in dir_path.get_files_list()]
|
|
118
|
+
|
|
119
|
+
def iterate_python_files(self, path_name: str, excluded_patterns: Optional[List[str]] = None) -> Iterator[Path]:
|
|
120
|
+
"""
|
|
121
|
+
Iterate over Python files in the specified path, excluding __init__.py files.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
path_name: Name of the path to search in
|
|
125
|
+
excluded_patterns: Additional patterns to exclude
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Iterator of Path objects for Python files
|
|
129
|
+
"""
|
|
130
|
+
path_info = self.get_path(path_name)
|
|
131
|
+
path = path_info["path"]
|
|
132
|
+
|
|
133
|
+
# Combine default and additional exclusion patterns
|
|
134
|
+
all_excluded_patterns = self._excluded_patterns.copy()
|
|
135
|
+
if excluded_patterns:
|
|
136
|
+
all_excluded_patterns.extend(excluded_patterns)
|
|
137
|
+
|
|
138
|
+
# Add __init__.py to excluded patterns
|
|
139
|
+
all_excluded_patterns.append("__init__.py")
|
|
140
|
+
|
|
141
|
+
# Use DirectoryPath to iterate over Python files
|
|
142
|
+
dir_path = DirectoryPath(path, all_excluded_patterns, [".py"])
|
|
143
|
+
return dir_path.iterate_files()
|
|
144
|
+
|
|
145
|
+
def get_drops(self, path_name: str) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Get the drops value for a path.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
path_name: Name of the path
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The drops value for the path
|
|
154
|
+
"""
|
|
155
|
+
path_info = self.get_path(path_name)
|
|
156
|
+
return path_info["drops"]
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_config(cls, config: Dict[str, Any]) -> "PathManager":
|
|
160
|
+
"""
|
|
161
|
+
Create a PathManager from configuration.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
config: Configuration object containing path information
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Initialized PathManager instance
|
|
168
|
+
"""
|
|
169
|
+
# Get use_pickle from config
|
|
170
|
+
|
|
171
|
+
use_pickle = config.get("parameters", {}).get("general", {}).get("use_pickle", False)
|
|
172
|
+
|
|
173
|
+
path_manager = cls(use_pickle=use_pickle)
|
|
174
|
+
|
|
175
|
+
config_paths = config.get("paths", {})
|
|
176
|
+
for path_name, path_info in config_paths.items():
|
|
177
|
+
path_manager.add_path(path_name, path_info["path"], path_info.get("drops", ""))
|
|
178
|
+
|
|
179
|
+
return path_manager
|
|
180
|
+
|
|
181
|
+
def find_parent_path(self, path: str) -> Optional[str]:
|
|
182
|
+
"""
|
|
183
|
+
Find the parent path from self.paths that contains the given path.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
path: The path to find the parent of
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The name of the parent path from self.paths that contains the given path,
|
|
190
|
+
or None if no parent is found
|
|
191
|
+
"""
|
|
192
|
+
path_obj = Path(path)
|
|
193
|
+
|
|
194
|
+
# Convert to the absolute path if needed
|
|
195
|
+
if not path_obj.is_absolute():
|
|
196
|
+
path_obj = self.root_dir / path_obj
|
|
197
|
+
|
|
198
|
+
# Search through all paths in self.paths to find the one that contains path_obj
|
|
199
|
+
best_match = None
|
|
200
|
+
best_match_depth = -1
|
|
201
|
+
|
|
202
|
+
for path_name, path_info in self.paths.items():
|
|
203
|
+
registered_path = Path(path_info["path"])
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# Check if path_obj is under this registered path
|
|
207
|
+
path_obj.relative_to(registered_path)
|
|
208
|
+
|
|
209
|
+
# Calculate depth (number of parent directories)
|
|
210
|
+
depth = len(registered_path.parts)
|
|
211
|
+
|
|
212
|
+
# Keep the deepest match (the most specific parent)
|
|
213
|
+
if depth > best_match_depth:
|
|
214
|
+
best_match = path_name
|
|
215
|
+
best_match_depth = depth
|
|
216
|
+
|
|
217
|
+
except ValueError:
|
|
218
|
+
# path_obj is not under this registered path, continue
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
return best_match
|
|
222
|
+
|
|
223
|
+
path_manager = PathManager()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def find_project_root(current_file=None, marker_files=(".git", "simstack.toml", "setup.py"),
|
|
6
|
+
skip_files=("simstack_marker.com",)) -> Path:
|
|
7
|
+
"""
|
|
8
|
+
Find the project root directory by searching for common marker files
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
skip_files: directories with these names will be skipped
|
|
12
|
+
current_file: Path to the current file, defaults to __file__ if None
|
|
13
|
+
marker_files: Tuple of files/directories that indicate the project root
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Absolute path to the project root directory
|
|
17
|
+
"""
|
|
18
|
+
if "SIMSTACK_PROJECT_ROOT" in os.environ:
|
|
19
|
+
return Path(os.environ["SIMSTACK_PROJECT_ROOT"]).resolve()
|
|
20
|
+
|
|
21
|
+
if current_file is None:
|
|
22
|
+
current_file = __file__
|
|
23
|
+
|
|
24
|
+
# Get the directory from where the process was started
|
|
25
|
+
current_dir = Path.cwd()
|
|
26
|
+
|
|
27
|
+
# Walk up the directory tree until we find a marker file
|
|
28
|
+
prev_dir = None
|
|
29
|
+
while current_dir != prev_dir:
|
|
30
|
+
# Check if any marker files/directories exist in the current directory
|
|
31
|
+
found_marker = any((current_dir / marker).exists() for marker in marker_files)
|
|
32
|
+
found_skip = any((current_dir / marker).exists() for marker in skip_files)
|
|
33
|
+
if found_marker and not found_skip:
|
|
34
|
+
return current_dir
|
|
35
|
+
|
|
36
|
+
# Move up one directory
|
|
37
|
+
prev_dir = current_dir
|
|
38
|
+
current_dir = current_dir.parent
|
|
39
|
+
|
|
40
|
+
# If we can't find any markers, return the directory of the current file
|
|
41
|
+
return Path(current_file).resolve().parent
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_spec(
|
|
14
|
+
cmd_file: Path
|
|
15
|
+
) -> Tuple[Optional[str], Dict[str, str], Optional[str], Optional[int]]:
|
|
16
|
+
cwd: Optional[str] = None
|
|
17
|
+
env: Dict[str, str] = {}
|
|
18
|
+
command: Optional[str] = None
|
|
19
|
+
timeout: Optional[int] = None
|
|
20
|
+
|
|
21
|
+
with cmd_file.open("r", encoding="utf-8") as f:
|
|
22
|
+
for raw in f:
|
|
23
|
+
line = raw.strip()
|
|
24
|
+
if not line:
|
|
25
|
+
continue
|
|
26
|
+
if line.startswith("CWD="):
|
|
27
|
+
cwd = line[len("CWD=") :]
|
|
28
|
+
elif line.startswith("ENV[") and "]=" in line:
|
|
29
|
+
key = line[len("ENV[") : line.index("]=", len("ENV["))]
|
|
30
|
+
val = line.split("]=", 1)[1]
|
|
31
|
+
env[key] = val
|
|
32
|
+
elif line.startswith("CMD="):
|
|
33
|
+
command = line[len("CMD=") :]
|
|
34
|
+
elif line.startswith("TIMEOUT="):
|
|
35
|
+
try:
|
|
36
|
+
timeout = int(line[len("TIMEOUT=") :])
|
|
37
|
+
except ValueError:
|
|
38
|
+
timeout = None
|
|
39
|
+
return cwd, env, command, timeout
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_command(
|
|
43
|
+
command: str,
|
|
44
|
+
cwd: Optional[str],
|
|
45
|
+
env: Dict[str, str],
|
|
46
|
+
timeout: Optional[int],
|
|
47
|
+
out_file: Path,
|
|
48
|
+
err_file: Path,
|
|
49
|
+
) -> int:
|
|
50
|
+
proc_env = os.environ.copy()
|
|
51
|
+
proc_env.update(env)
|
|
52
|
+
with out_file.open("w", encoding="utf-8") as out, err_file.open(
|
|
53
|
+
"w", encoding="utf-8"
|
|
54
|
+
) as err:
|
|
55
|
+
try:
|
|
56
|
+
proc = subprocess.run(
|
|
57
|
+
command,
|
|
58
|
+
shell=True,
|
|
59
|
+
cwd=cwd if cwd else None,
|
|
60
|
+
env=proc_env,
|
|
61
|
+
stdout=out,
|
|
62
|
+
stderr=err,
|
|
63
|
+
text=True,
|
|
64
|
+
timeout=timeout if (timeout and timeout > 0) else None,
|
|
65
|
+
check=False,
|
|
66
|
+
)
|
|
67
|
+
return proc.returncode
|
|
68
|
+
except subprocess.TimeoutExpired:
|
|
69
|
+
err.write("\n[watcher] Command timed out\n")
|
|
70
|
+
return 124
|
|
71
|
+
except Exception as e:
|
|
72
|
+
err.write(f"\n[watcher] Execution error: {e}\n")
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def process_job_from_claim(base_stem: Path, claimed_by_lock: bool) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Process a job using base path (without suffix). If claimed_by_lock is True, we did not
|
|
79
|
+
rename .ready; we will remove it at the end. Otherwise, we operate on .claim and remove it.
|
|
80
|
+
"""
|
|
81
|
+
cmd_file = base_stem.with_suffix(".cmd")
|
|
82
|
+
ready_file = base_stem.with_suffix(".ready")
|
|
83
|
+
claim_file = base_stem.with_suffix(".claim")
|
|
84
|
+
lock_file = base_stem.with_suffix(".lock")
|
|
85
|
+
out_file = base_stem.with_suffix(".stdout")
|
|
86
|
+
err_file = base_stem.with_suffix(".stderr")
|
|
87
|
+
exit_file = base_stem.with_suffix(".exit")
|
|
88
|
+
done_file = base_stem.with_suffix(".done")
|
|
89
|
+
fail_file = base_stem.with_suffix(".fail")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
if not cmd_file.exists():
|
|
93
|
+
fail_file.touch()
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
cwd, env, command, timeout = parse_spec(cmd_file)
|
|
97
|
+
if not command:
|
|
98
|
+
err_file.write_text("[watcher] CMD missing in spec\n", encoding="utf-8")
|
|
99
|
+
exit_file.write_text("2\n", encoding="utf-8")
|
|
100
|
+
fail_file.touch()
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
rc = run_command(command, cwd, env, timeout, out_file, err_file)
|
|
104
|
+
print(f"[watcher] Job {base_stem.name} finished with rc={rc}")
|
|
105
|
+
exit_file.write_text(f"{rc}\n", encoding="utf-8")
|
|
106
|
+
print(f"[watcher] Job {base_stem.name} finished")
|
|
107
|
+
(done_file if rc == 0 else fail_file).touch()
|
|
108
|
+
finally:
|
|
109
|
+
# Cleanup markers depending on the claim type
|
|
110
|
+
if claimed_by_lock:
|
|
111
|
+
try:
|
|
112
|
+
if ready_file.exists():
|
|
113
|
+
ready_file.unlink()
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(
|
|
116
|
+
f"[watcher] Warning: failed to remove ready {ready_file}: {e}",
|
|
117
|
+
file=sys.stderr,
|
|
118
|
+
)
|
|
119
|
+
try:
|
|
120
|
+
if lock_file.exists():
|
|
121
|
+
lock_file.unlink()
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(
|
|
124
|
+
f"[watcher] Warning: failed to remove lock {lock_file}: {e}",
|
|
125
|
+
file=sys.stderr,
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
try:
|
|
129
|
+
if claim_file.exists():
|
|
130
|
+
claim_file.unlink()
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(
|
|
133
|
+
f"[watcher] Warning: failed to remove claim {claim_file}: {e}",
|
|
134
|
+
file=sys.stderr,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def try_claim(
|
|
139
|
+
ready_file: Path, retries: int = 5, backoff: float = 0.05
|
|
140
|
+
) -> Optional[Path]:
|
|
141
|
+
"""
|
|
142
|
+
Atomically rename *.ready -> *.claim. Retries on transient failures.
|
|
143
|
+
Returns claim path if successful, else None.
|
|
144
|
+
"""
|
|
145
|
+
claim = ready_file.with_suffix(".claim")
|
|
146
|
+
for i in range(max(1, retries)):
|
|
147
|
+
try:
|
|
148
|
+
os.replace(ready_file, claim)
|
|
149
|
+
return claim
|
|
150
|
+
except FileNotFoundError:
|
|
151
|
+
return None
|
|
152
|
+
except PermissionError:
|
|
153
|
+
# Windows/NFS transient locks: backoff and retry
|
|
154
|
+
time.sleep(backoff * (i + 1))
|
|
155
|
+
except OSError:
|
|
156
|
+
# Other transient FS issues: backoff and retry
|
|
157
|
+
time.sleep(backoff * (i + 1))
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def try_lock(base_stem: Path) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Create an exclusive lock file <id>.lock using O_CREAT|O_EXCL.
|
|
164
|
+
Returns True if lock acquired, False otherwise.
|
|
165
|
+
"""
|
|
166
|
+
lock_path = base_stem.with_suffix(".lock")
|
|
167
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
|
|
168
|
+
try:
|
|
169
|
+
fd = os.open(lock_path, flags, 0o644)
|
|
170
|
+
os.close(fd)
|
|
171
|
+
return True
|
|
172
|
+
except FileExistsError:
|
|
173
|
+
return False
|
|
174
|
+
except OSError as e:
|
|
175
|
+
print(f"[watcher] Lock create failed for {lock_path}: {e}", file=sys.stderr)
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main() -> int:
|
|
180
|
+
parser = argparse.ArgumentParser(
|
|
181
|
+
description="Concurrent file-queue watcher with robust claiming"
|
|
182
|
+
)
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--queue-dir", type=Path, required=False, default=Path.cwd() / "queue"
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument("--poll-interval", type=float, default=1.0)
|
|
187
|
+
parser.add_argument("--workers", type=int, default=min(4, (os.cpu_count() or 2)))
|
|
188
|
+
parser.add_argument(
|
|
189
|
+
"--use-srun",
|
|
190
|
+
action="store_true",
|
|
191
|
+
help="(deprecated; wrapper removed for simplicity)",
|
|
192
|
+
)
|
|
193
|
+
parser.add_argument("--srun-args", type=str, default="")
|
|
194
|
+
parser.add_argument(
|
|
195
|
+
"--once", action="store_true", help="Process current claims and exit"
|
|
196
|
+
)
|
|
197
|
+
args = parser.parse_args()
|
|
198
|
+
|
|
199
|
+
qd = args.queue_dir
|
|
200
|
+
qd.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
print(f"[watcher] Watching {qd}, workers={args.workers}")
|
|
202
|
+
|
|
203
|
+
stop = {"flag": False}
|
|
204
|
+
|
|
205
|
+
def handle_sig(_s, _f):
|
|
206
|
+
stop["flag"] = True
|
|
207
|
+
|
|
208
|
+
signal.signal(signal.SIGINT, handle_sig)
|
|
209
|
+
signal.signal(signal.SIGTERM, handle_sig)
|
|
210
|
+
|
|
211
|
+
with ThreadPoolExecutor(max_workers=max(1, args.workers)) as pool:
|
|
212
|
+
futures = set()
|
|
213
|
+
|
|
214
|
+
while not stop["flag"]:
|
|
215
|
+
claimed_any = False
|
|
216
|
+
|
|
217
|
+
# Scan for all .ready files
|
|
218
|
+
ready_list = list(qd.glob("*.ready"))
|
|
219
|
+
if ready_list:
|
|
220
|
+
print(f"[watcher] Found {len(ready_list)} ready file(s)")
|
|
221
|
+
for ready in ready_list:
|
|
222
|
+
base_stem = ready.with_suffix("") # drop .ready
|
|
223
|
+
# 1) Preferred: atomic rename to .claim (with retries)
|
|
224
|
+
claim = try_claim(ready)
|
|
225
|
+
if claim is not None:
|
|
226
|
+
claimed_any = True
|
|
227
|
+
futures.add(pool.submit(process_job_from_claim, base_stem, False))
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
# 2) Fallback: exclusive lock file if rename failed
|
|
231
|
+
if try_lock(base_stem):
|
|
232
|
+
print(f"[watcher] Claimed by lock: {base_stem.name}")
|
|
233
|
+
claimed_any = True
|
|
234
|
+
futures.add(pool.submit(process_job_from_claim, base_stem, True))
|
|
235
|
+
|
|
236
|
+
# Reap done tasks
|
|
237
|
+
done_now = {f for f in futures if f.done()}
|
|
238
|
+
for f in done_now:
|
|
239
|
+
try:
|
|
240
|
+
f.result()
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print(f"[watcher] Job raised: {e}", file=sys.stderr)
|
|
243
|
+
futures.discard(f)
|
|
244
|
+
|
|
245
|
+
if args.once:
|
|
246
|
+
# Drain: wait for all running tasks to finish
|
|
247
|
+
for f in as_completed(list(futures)):
|
|
248
|
+
try:
|
|
249
|
+
f.result()
|
|
250
|
+
except Exception as e:
|
|
251
|
+
print(f"[watcher] Job raised: {e}", file=sys.stderr)
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
if not claimed_any:
|
|
255
|
+
time.sleep(max(0.05, args.poll_interval))
|
|
256
|
+
|
|
257
|
+
print("[watcher] Exiting")
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__":
|
|
262
|
+
sys.exit(main())
|