nvidia-nat 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiq/__init__.py +66 -0
- nat/agent/__init__.py +0 -0
- nat/agent/base.py +256 -0
- nat/agent/dual_node.py +67 -0
- nat/agent/react_agent/__init__.py +0 -0
- nat/agent/react_agent/agent.py +363 -0
- nat/agent/react_agent/output_parser.py +104 -0
- nat/agent/react_agent/prompt.py +44 -0
- nat/agent/react_agent/register.py +149 -0
- nat/agent/reasoning_agent/__init__.py +0 -0
- nat/agent/reasoning_agent/reasoning_agent.py +225 -0
- nat/agent/register.py +23 -0
- nat/agent/rewoo_agent/__init__.py +0 -0
- nat/agent/rewoo_agent/agent.py +415 -0
- nat/agent/rewoo_agent/prompt.py +110 -0
- nat/agent/rewoo_agent/register.py +157 -0
- nat/agent/tool_calling_agent/__init__.py +0 -0
- nat/agent/tool_calling_agent/agent.py +119 -0
- nat/agent/tool_calling_agent/register.py +106 -0
- nat/authentication/__init__.py +14 -0
- nat/authentication/api_key/__init__.py +14 -0
- nat/authentication/api_key/api_key_auth_provider.py +96 -0
- nat/authentication/api_key/api_key_auth_provider_config.py +124 -0
- nat/authentication/api_key/register.py +26 -0
- nat/authentication/exceptions/__init__.py +14 -0
- nat/authentication/exceptions/api_key_exceptions.py +38 -0
- nat/authentication/http_basic_auth/__init__.py +0 -0
- nat/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
- nat/authentication/http_basic_auth/register.py +30 -0
- nat/authentication/interfaces.py +93 -0
- nat/authentication/oauth2/__init__.py +14 -0
- nat/authentication/oauth2/oauth2_auth_code_flow_provider.py +107 -0
- nat/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
- nat/authentication/oauth2/register.py +25 -0
- nat/authentication/register.py +21 -0
- nat/builder/__init__.py +0 -0
- nat/builder/builder.py +285 -0
- nat/builder/component_utils.py +316 -0
- nat/builder/context.py +270 -0
- nat/builder/embedder.py +24 -0
- nat/builder/eval_builder.py +161 -0
- nat/builder/evaluator.py +29 -0
- nat/builder/framework_enum.py +24 -0
- nat/builder/front_end.py +73 -0
- nat/builder/function.py +344 -0
- nat/builder/function_base.py +380 -0
- nat/builder/function_info.py +627 -0
- nat/builder/intermediate_step_manager.py +174 -0
- nat/builder/llm.py +25 -0
- nat/builder/retriever.py +25 -0
- nat/builder/user_interaction_manager.py +78 -0
- nat/builder/workflow.py +148 -0
- nat/builder/workflow_builder.py +1117 -0
- nat/cli/__init__.py +14 -0
- nat/cli/cli_utils/__init__.py +0 -0
- nat/cli/cli_utils/config_override.py +231 -0
- nat/cli/cli_utils/validation.py +37 -0
- nat/cli/commands/__init__.py +0 -0
- nat/cli/commands/configure/__init__.py +0 -0
- nat/cli/commands/configure/channel/__init__.py +0 -0
- nat/cli/commands/configure/channel/add.py +28 -0
- nat/cli/commands/configure/channel/channel.py +34 -0
- nat/cli/commands/configure/channel/remove.py +30 -0
- nat/cli/commands/configure/channel/update.py +30 -0
- nat/cli/commands/configure/configure.py +33 -0
- nat/cli/commands/evaluate.py +139 -0
- nat/cli/commands/info/__init__.py +14 -0
- nat/cli/commands/info/info.py +37 -0
- nat/cli/commands/info/list_channels.py +32 -0
- nat/cli/commands/info/list_components.py +129 -0
- nat/cli/commands/info/list_mcp.py +304 -0
- nat/cli/commands/registry/__init__.py +14 -0
- nat/cli/commands/registry/publish.py +88 -0
- nat/cli/commands/registry/pull.py +118 -0
- nat/cli/commands/registry/registry.py +36 -0
- nat/cli/commands/registry/remove.py +108 -0
- nat/cli/commands/registry/search.py +155 -0
- nat/cli/commands/sizing/__init__.py +14 -0
- nat/cli/commands/sizing/calc.py +297 -0
- nat/cli/commands/sizing/sizing.py +27 -0
- nat/cli/commands/start.py +246 -0
- nat/cli/commands/uninstall.py +81 -0
- nat/cli/commands/validate.py +47 -0
- nat/cli/commands/workflow/__init__.py +14 -0
- nat/cli/commands/workflow/templates/__init__.py.j2 +0 -0
- nat/cli/commands/workflow/templates/config.yml.j2 +16 -0
- nat/cli/commands/workflow/templates/pyproject.toml.j2 +22 -0
- nat/cli/commands/workflow/templates/register.py.j2 +5 -0
- nat/cli/commands/workflow/templates/workflow.py.j2 +36 -0
- nat/cli/commands/workflow/workflow.py +37 -0
- nat/cli/commands/workflow/workflow_commands.py +317 -0
- nat/cli/entrypoint.py +135 -0
- nat/cli/main.py +57 -0
- nat/cli/register_workflow.py +488 -0
- nat/cli/type_registry.py +1000 -0
- nat/data_models/__init__.py +14 -0
- nat/data_models/api_server.py +716 -0
- nat/data_models/authentication.py +231 -0
- nat/data_models/common.py +171 -0
- nat/data_models/component.py +58 -0
- nat/data_models/component_ref.py +168 -0
- nat/data_models/config.py +410 -0
- nat/data_models/dataset_handler.py +169 -0
- nat/data_models/discovery_metadata.py +305 -0
- nat/data_models/embedder.py +27 -0
- nat/data_models/evaluate.py +127 -0
- nat/data_models/evaluator.py +26 -0
- nat/data_models/front_end.py +26 -0
- nat/data_models/function.py +30 -0
- nat/data_models/function_dependencies.py +72 -0
- nat/data_models/interactive.py +246 -0
- nat/data_models/intermediate_step.py +302 -0
- nat/data_models/invocation_node.py +38 -0
- nat/data_models/llm.py +27 -0
- nat/data_models/logging.py +26 -0
- nat/data_models/memory.py +27 -0
- nat/data_models/object_store.py +44 -0
- nat/data_models/profiler.py +54 -0
- nat/data_models/registry_handler.py +26 -0
- nat/data_models/retriever.py +30 -0
- nat/data_models/retry_mixin.py +35 -0
- nat/data_models/span.py +190 -0
- nat/data_models/step_adaptor.py +64 -0
- nat/data_models/streaming.py +33 -0
- nat/data_models/swe_bench_model.py +54 -0
- nat/data_models/telemetry_exporter.py +26 -0
- nat/data_models/ttc_strategy.py +30 -0
- nat/embedder/__init__.py +0 -0
- nat/embedder/nim_embedder.py +59 -0
- nat/embedder/openai_embedder.py +43 -0
- nat/embedder/register.py +22 -0
- nat/eval/__init__.py +14 -0
- nat/eval/config.py +60 -0
- nat/eval/dataset_handler/__init__.py +0 -0
- nat/eval/dataset_handler/dataset_downloader.py +106 -0
- nat/eval/dataset_handler/dataset_filter.py +52 -0
- nat/eval/dataset_handler/dataset_handler.py +367 -0
- nat/eval/evaluate.py +510 -0
- nat/eval/evaluator/__init__.py +14 -0
- nat/eval/evaluator/base_evaluator.py +77 -0
- nat/eval/evaluator/evaluator_model.py +45 -0
- nat/eval/intermediate_step_adapter.py +99 -0
- nat/eval/rag_evaluator/__init__.py +0 -0
- nat/eval/rag_evaluator/evaluate.py +178 -0
- nat/eval/rag_evaluator/register.py +143 -0
- nat/eval/register.py +23 -0
- nat/eval/remote_workflow.py +133 -0
- nat/eval/runners/__init__.py +14 -0
- nat/eval/runners/config.py +39 -0
- nat/eval/runners/multi_eval_runner.py +54 -0
- nat/eval/runtime_event_subscriber.py +52 -0
- nat/eval/swe_bench_evaluator/__init__.py +0 -0
- nat/eval/swe_bench_evaluator/evaluate.py +215 -0
- nat/eval/swe_bench_evaluator/register.py +36 -0
- nat/eval/trajectory_evaluator/__init__.py +0 -0
- nat/eval/trajectory_evaluator/evaluate.py +75 -0
- nat/eval/trajectory_evaluator/register.py +40 -0
- nat/eval/tunable_rag_evaluator/__init__.py +0 -0
- nat/eval/tunable_rag_evaluator/evaluate.py +245 -0
- nat/eval/tunable_rag_evaluator/register.py +52 -0
- nat/eval/usage_stats.py +41 -0
- nat/eval/utils/__init__.py +0 -0
- nat/eval/utils/output_uploader.py +140 -0
- nat/eval/utils/tqdm_position_registry.py +40 -0
- nat/eval/utils/weave_eval.py +184 -0
- nat/experimental/__init__.py +0 -0
- nat/experimental/decorators/__init__.py +0 -0
- nat/experimental/decorators/experimental_warning_decorator.py +134 -0
- nat/experimental/test_time_compute/__init__.py +0 -0
- nat/experimental/test_time_compute/editing/__init__.py +0 -0
- nat/experimental/test_time_compute/editing/iterative_plan_refinement_editor.py +147 -0
- nat/experimental/test_time_compute/editing/llm_as_a_judge_editor.py +204 -0
- nat/experimental/test_time_compute/editing/motivation_aware_summarization.py +107 -0
- nat/experimental/test_time_compute/functions/__init__.py +0 -0
- nat/experimental/test_time_compute/functions/execute_score_select_function.py +105 -0
- nat/experimental/test_time_compute/functions/plan_select_execute_function.py +224 -0
- nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +205 -0
- nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +146 -0
- nat/experimental/test_time_compute/models/__init__.py +0 -0
- nat/experimental/test_time_compute/models/editor_config.py +132 -0
- nat/experimental/test_time_compute/models/scoring_config.py +112 -0
- nat/experimental/test_time_compute/models/search_config.py +120 -0
- nat/experimental/test_time_compute/models/selection_config.py +154 -0
- nat/experimental/test_time_compute/models/stage_enums.py +43 -0
- nat/experimental/test_time_compute/models/strategy_base.py +66 -0
- nat/experimental/test_time_compute/models/tool_use_config.py +41 -0
- nat/experimental/test_time_compute/models/ttc_item.py +48 -0
- nat/experimental/test_time_compute/register.py +36 -0
- nat/experimental/test_time_compute/scoring/__init__.py +0 -0
- nat/experimental/test_time_compute/scoring/llm_based_agent_scorer.py +168 -0
- nat/experimental/test_time_compute/scoring/llm_based_plan_scorer.py +168 -0
- nat/experimental/test_time_compute/scoring/motivation_aware_scorer.py +111 -0
- nat/experimental/test_time_compute/search/__init__.py +0 -0
- nat/experimental/test_time_compute/search/multi_llm_planner.py +128 -0
- nat/experimental/test_time_compute/search/multi_query_retrieval_search.py +122 -0
- nat/experimental/test_time_compute/search/single_shot_multi_plan_planner.py +128 -0
- nat/experimental/test_time_compute/selection/__init__.py +0 -0
- nat/experimental/test_time_compute/selection/best_of_n_selector.py +63 -0
- nat/experimental/test_time_compute/selection/llm_based_agent_output_selector.py +131 -0
- nat/experimental/test_time_compute/selection/llm_based_output_merging_selector.py +159 -0
- nat/experimental/test_time_compute/selection/llm_based_plan_selector.py +128 -0
- nat/experimental/test_time_compute/selection/threshold_selector.py +58 -0
- nat/front_ends/__init__.py +14 -0
- nat/front_ends/console/__init__.py +14 -0
- nat/front_ends/console/authentication_flow_handler.py +233 -0
- nat/front_ends/console/console_front_end_config.py +32 -0
- nat/front_ends/console/console_front_end_plugin.py +96 -0
- nat/front_ends/console/register.py +25 -0
- nat/front_ends/cron/__init__.py +14 -0
- nat/front_ends/fastapi/__init__.py +14 -0
- nat/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
- nat/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
- nat/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +107 -0
- nat/front_ends/fastapi/fastapi_front_end_config.py +241 -0
- nat/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
- nat/front_ends/fastapi/fastapi_front_end_plugin.py +116 -0
- nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +1087 -0
- nat/front_ends/fastapi/html_snippets/__init__.py +14 -0
- nat/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
- nat/front_ends/fastapi/intermediate_steps_subscriber.py +80 -0
- nat/front_ends/fastapi/job_store.py +183 -0
- nat/front_ends/fastapi/main.py +72 -0
- nat/front_ends/fastapi/message_handler.py +320 -0
- nat/front_ends/fastapi/message_validator.py +352 -0
- nat/front_ends/fastapi/register.py +25 -0
- nat/front_ends/fastapi/response_helpers.py +195 -0
- nat/front_ends/fastapi/step_adaptor.py +319 -0
- nat/front_ends/mcp/__init__.py +14 -0
- nat/front_ends/mcp/mcp_front_end_config.py +36 -0
- nat/front_ends/mcp/mcp_front_end_plugin.py +81 -0
- nat/front_ends/mcp/mcp_front_end_plugin_worker.py +143 -0
- nat/front_ends/mcp/register.py +27 -0
- nat/front_ends/mcp/tool_converter.py +241 -0
- nat/front_ends/register.py +22 -0
- nat/front_ends/simple_base/__init__.py +14 -0
- nat/front_ends/simple_base/simple_front_end_plugin_base.py +54 -0
- nat/llm/__init__.py +0 -0
- nat/llm/aws_bedrock_llm.py +57 -0
- nat/llm/nim_llm.py +46 -0
- nat/llm/openai_llm.py +46 -0
- nat/llm/register.py +23 -0
- nat/llm/utils/__init__.py +14 -0
- nat/llm/utils/env_config_value.py +94 -0
- nat/llm/utils/error.py +17 -0
- nat/memory/__init__.py +20 -0
- nat/memory/interfaces.py +183 -0
- nat/memory/models.py +112 -0
- nat/meta/pypi.md +58 -0
- nat/object_store/__init__.py +20 -0
- nat/object_store/in_memory_object_store.py +76 -0
- nat/object_store/interfaces.py +84 -0
- nat/object_store/models.py +38 -0
- nat/object_store/register.py +20 -0
- nat/observability/__init__.py +14 -0
- nat/observability/exporter/__init__.py +14 -0
- nat/observability/exporter/base_exporter.py +449 -0
- nat/observability/exporter/exporter.py +78 -0
- nat/observability/exporter/file_exporter.py +33 -0
- nat/observability/exporter/processing_exporter.py +322 -0
- nat/observability/exporter/raw_exporter.py +52 -0
- nat/observability/exporter/span_exporter.py +288 -0
- nat/observability/exporter_manager.py +335 -0
- nat/observability/mixin/__init__.py +14 -0
- nat/observability/mixin/batch_config_mixin.py +26 -0
- nat/observability/mixin/collector_config_mixin.py +23 -0
- nat/observability/mixin/file_mixin.py +288 -0
- nat/observability/mixin/file_mode.py +23 -0
- nat/observability/mixin/resource_conflict_mixin.py +134 -0
- nat/observability/mixin/serialize_mixin.py +61 -0
- nat/observability/mixin/type_introspection_mixin.py +183 -0
- nat/observability/processor/__init__.py +14 -0
- nat/observability/processor/batching_processor.py +310 -0
- nat/observability/processor/callback_processor.py +42 -0
- nat/observability/processor/intermediate_step_serializer.py +28 -0
- nat/observability/processor/processor.py +71 -0
- nat/observability/register.py +96 -0
- nat/observability/utils/__init__.py +14 -0
- nat/observability/utils/dict_utils.py +236 -0
- nat/observability/utils/time_utils.py +31 -0
- nat/plugins/.namespace +1 -0
- nat/profiler/__init__.py +0 -0
- nat/profiler/calc/__init__.py +14 -0
- nat/profiler/calc/calc_runner.py +627 -0
- nat/profiler/calc/calculations.py +288 -0
- nat/profiler/calc/data_models.py +188 -0
- nat/profiler/calc/plot.py +345 -0
- nat/profiler/callbacks/__init__.py +0 -0
- nat/profiler/callbacks/agno_callback_handler.py +295 -0
- nat/profiler/callbacks/base_callback_class.py +20 -0
- nat/profiler/callbacks/langchain_callback_handler.py +290 -0
- nat/profiler/callbacks/llama_index_callback_handler.py +205 -0
- nat/profiler/callbacks/semantic_kernel_callback_handler.py +238 -0
- nat/profiler/callbacks/token_usage_base_model.py +27 -0
- nat/profiler/data_frame_row.py +51 -0
- nat/profiler/data_models.py +24 -0
- nat/profiler/decorators/__init__.py +0 -0
- nat/profiler/decorators/framework_wrapper.py +131 -0
- nat/profiler/decorators/function_tracking.py +254 -0
- nat/profiler/forecasting/__init__.py +0 -0
- nat/profiler/forecasting/config.py +18 -0
- nat/profiler/forecasting/model_trainer.py +75 -0
- nat/profiler/forecasting/models/__init__.py +22 -0
- nat/profiler/forecasting/models/forecasting_base_model.py +40 -0
- nat/profiler/forecasting/models/linear_model.py +197 -0
- nat/profiler/forecasting/models/random_forest_regressor.py +269 -0
- nat/profiler/inference_metrics_model.py +28 -0
- nat/profiler/inference_optimization/__init__.py +0 -0
- nat/profiler/inference_optimization/bottleneck_analysis/__init__.py +0 -0
- nat/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +460 -0
- nat/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +258 -0
- nat/profiler/inference_optimization/data_models.py +386 -0
- nat/profiler/inference_optimization/experimental/__init__.py +0 -0
- nat/profiler/inference_optimization/experimental/concurrency_spike_analysis.py +468 -0
- nat/profiler/inference_optimization/experimental/prefix_span_analysis.py +405 -0
- nat/profiler/inference_optimization/llm_metrics.py +212 -0
- nat/profiler/inference_optimization/prompt_caching.py +163 -0
- nat/profiler/inference_optimization/token_uniqueness.py +107 -0
- nat/profiler/inference_optimization/workflow_runtimes.py +72 -0
- nat/profiler/intermediate_property_adapter.py +102 -0
- nat/profiler/profile_runner.py +473 -0
- nat/profiler/utils.py +184 -0
- nat/registry_handlers/__init__.py +0 -0
- nat/registry_handlers/local/__init__.py +0 -0
- nat/registry_handlers/local/local_handler.py +176 -0
- nat/registry_handlers/local/register_local.py +37 -0
- nat/registry_handlers/metadata_factory.py +60 -0
- nat/registry_handlers/package_utils.py +571 -0
- nat/registry_handlers/pypi/__init__.py +0 -0
- nat/registry_handlers/pypi/pypi_handler.py +251 -0
- nat/registry_handlers/pypi/register_pypi.py +40 -0
- nat/registry_handlers/register.py +21 -0
- nat/registry_handlers/registry_handler_base.py +157 -0
- nat/registry_handlers/rest/__init__.py +0 -0
- nat/registry_handlers/rest/register_rest.py +56 -0
- nat/registry_handlers/rest/rest_handler.py +237 -0
- nat/registry_handlers/schemas/__init__.py +0 -0
- nat/registry_handlers/schemas/headers.py +42 -0
- nat/registry_handlers/schemas/package.py +68 -0
- nat/registry_handlers/schemas/publish.py +68 -0
- nat/registry_handlers/schemas/pull.py +82 -0
- nat/registry_handlers/schemas/remove.py +36 -0
- nat/registry_handlers/schemas/search.py +91 -0
- nat/registry_handlers/schemas/status.py +47 -0
- nat/retriever/__init__.py +0 -0
- nat/retriever/interface.py +41 -0
- nat/retriever/milvus/__init__.py +14 -0
- nat/retriever/milvus/register.py +81 -0
- nat/retriever/milvus/retriever.py +228 -0
- nat/retriever/models.py +77 -0
- nat/retriever/nemo_retriever/__init__.py +14 -0
- nat/retriever/nemo_retriever/register.py +60 -0
- nat/retriever/nemo_retriever/retriever.py +190 -0
- nat/retriever/register.py +22 -0
- nat/runtime/__init__.py +14 -0
- nat/runtime/loader.py +220 -0
- nat/runtime/runner.py +195 -0
- nat/runtime/session.py +162 -0
- nat/runtime/user_metadata.py +130 -0
- nat/settings/__init__.py +0 -0
- nat/settings/global_settings.py +318 -0
- nat/test/.namespace +1 -0
- nat/tool/__init__.py +0 -0
- nat/tool/chat_completion.py +74 -0
- nat/tool/code_execution/README.md +151 -0
- nat/tool/code_execution/__init__.py +0 -0
- nat/tool/code_execution/code_sandbox.py +267 -0
- nat/tool/code_execution/local_sandbox/.gitignore +1 -0
- nat/tool/code_execution/local_sandbox/Dockerfile.sandbox +60 -0
- nat/tool/code_execution/local_sandbox/__init__.py +13 -0
- nat/tool/code_execution/local_sandbox/local_sandbox_server.py +198 -0
- nat/tool/code_execution/local_sandbox/sandbox.requirements.txt +6 -0
- nat/tool/code_execution/local_sandbox/start_local_sandbox.sh +50 -0
- nat/tool/code_execution/register.py +74 -0
- nat/tool/code_execution/test_code_execution_sandbox.py +414 -0
- nat/tool/code_execution/utils.py +100 -0
- nat/tool/datetime_tools.py +42 -0
- nat/tool/document_search.py +141 -0
- nat/tool/github_tools/__init__.py +0 -0
- nat/tool/github_tools/create_github_commit.py +133 -0
- nat/tool/github_tools/create_github_issue.py +87 -0
- nat/tool/github_tools/create_github_pr.py +106 -0
- nat/tool/github_tools/get_github_file.py +106 -0
- nat/tool/github_tools/get_github_issue.py +166 -0
- nat/tool/github_tools/get_github_pr.py +256 -0
- nat/tool/github_tools/update_github_issue.py +100 -0
- nat/tool/mcp/__init__.py +14 -0
- nat/tool/mcp/exceptions.py +142 -0
- nat/tool/mcp/mcp_client.py +255 -0
- nat/tool/mcp/mcp_tool.py +96 -0
- nat/tool/memory_tools/__init__.py +0 -0
- nat/tool/memory_tools/add_memory_tool.py +79 -0
- nat/tool/memory_tools/delete_memory_tool.py +67 -0
- nat/tool/memory_tools/get_memory_tool.py +72 -0
- nat/tool/nvidia_rag.py +95 -0
- nat/tool/register.py +38 -0
- nat/tool/retriever.py +94 -0
- nat/tool/server_tools.py +66 -0
- nat/utils/__init__.py +0 -0
- nat/utils/data_models/__init__.py +0 -0
- nat/utils/data_models/schema_validator.py +58 -0
- nat/utils/debugging_utils.py +43 -0
- nat/utils/dump_distro_mapping.py +32 -0
- nat/utils/exception_handlers/__init__.py +0 -0
- nat/utils/exception_handlers/automatic_retries.py +289 -0
- nat/utils/exception_handlers/mcp.py +211 -0
- nat/utils/exception_handlers/schemas.py +114 -0
- nat/utils/io/__init__.py +0 -0
- nat/utils/io/model_processing.py +28 -0
- nat/utils/io/yaml_tools.py +119 -0
- nat/utils/log_utils.py +37 -0
- nat/utils/metadata_utils.py +74 -0
- nat/utils/optional_imports.py +142 -0
- nat/utils/producer_consumer_queue.py +178 -0
- nat/utils/reactive/__init__.py +0 -0
- nat/utils/reactive/base/__init__.py +0 -0
- nat/utils/reactive/base/observable_base.py +65 -0
- nat/utils/reactive/base/observer_base.py +55 -0
- nat/utils/reactive/base/subject_base.py +79 -0
- nat/utils/reactive/observable.py +59 -0
- nat/utils/reactive/observer.py +76 -0
- nat/utils/reactive/subject.py +131 -0
- nat/utils/reactive/subscription.py +49 -0
- nat/utils/settings/__init__.py +0 -0
- nat/utils/settings/global_settings.py +197 -0
- nat/utils/string_utils.py +38 -0
- nat/utils/type_converter.py +290 -0
- nat/utils/type_utils.py +484 -0
- nat/utils/url_utils.py +27 -0
- nvidia_nat-1.2.0.dist-info/METADATA +365 -0
- nvidia_nat-1.2.0.dist-info/RECORD +435 -0
- nvidia_nat-1.2.0.dist-info/WHEEL +5 -0
- nvidia_nat-1.2.0.dist-info/entry_points.txt +21 -0
- nvidia_nat-1.2.0.dist-info/licenses/LICENSE-3rd-party.txt +5478 -0
- nvidia_nat-1.2.0.dist-info/licenses/LICENSE.md +201 -0
- nvidia_nat-1.2.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
|
|
20
|
+
from nat.builder.context import ContextState
|
|
21
|
+
from nat.observability.exporter.base_exporter import BaseExporter
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExporterManager:
|
|
27
|
+
"""
|
|
28
|
+
Manages the lifecycle of asynchronous exporters.
|
|
29
|
+
|
|
30
|
+
ExporterManager maintains a registry of exporters, allowing for dynamic addition and removal. It provides
|
|
31
|
+
methods to start and stop all registered exporters concurrently, ensuring proper synchronization and
|
|
32
|
+
lifecycle management. The manager is designed to prevent race conditions during exporter operations and to
|
|
33
|
+
handle exporter tasks in an asyncio event loop.
|
|
34
|
+
|
|
35
|
+
Each workflow execution gets its own ExporterManager instance to manage the lifecycle of exporters
|
|
36
|
+
during that workflow's execution.
|
|
37
|
+
|
|
38
|
+
Exporters added after `start()` is called will not be started automatically. They will only be
|
|
39
|
+
started on the next lifecycle (i.e., after a stop and subsequent start).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
shutdown_timeout (int, optional): Maximum time in seconds to wait for exporters to shut down gracefully.
|
|
43
|
+
Defaults to 120 seconds.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, shutdown_timeout: int = 120):
|
|
47
|
+
"""Initialize the ExporterManager."""
|
|
48
|
+
self._tasks: dict[str, asyncio.Task] = {}
|
|
49
|
+
self._running: bool = False
|
|
50
|
+
self._exporter_registry: dict[str, BaseExporter] = {}
|
|
51
|
+
self._is_registry_shared: bool = False
|
|
52
|
+
self._lock: asyncio.Lock = asyncio.Lock()
|
|
53
|
+
self._shutdown_event: asyncio.Event = asyncio.Event()
|
|
54
|
+
self._shutdown_timeout: int = shutdown_timeout
|
|
55
|
+
# Track isolated exporters for proper cleanup
|
|
56
|
+
self._active_isolated_exporters: dict[str, BaseExporter] = {}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def _create_with_shared_registry(cls, shutdown_timeout: int,
|
|
60
|
+
shared_registry: dict[str, BaseExporter]) -> "ExporterManager":
|
|
61
|
+
"""Internal factory method for creating instances with shared registry."""
|
|
62
|
+
instance = cls.__new__(cls)
|
|
63
|
+
instance._tasks = {}
|
|
64
|
+
instance._running = False
|
|
65
|
+
instance._exporter_registry = shared_registry
|
|
66
|
+
instance._is_registry_shared = True
|
|
67
|
+
instance._lock = asyncio.Lock()
|
|
68
|
+
instance._shutdown_event = asyncio.Event()
|
|
69
|
+
instance._shutdown_timeout = shutdown_timeout
|
|
70
|
+
instance._active_isolated_exporters = {}
|
|
71
|
+
return instance
|
|
72
|
+
|
|
73
|
+
def _ensure_registry_owned(self):
|
|
74
|
+
"""Ensure we own the registry (copy-on-write)."""
|
|
75
|
+
if self._is_registry_shared:
|
|
76
|
+
self._exporter_registry = self._exporter_registry.copy()
|
|
77
|
+
self._is_registry_shared = False
|
|
78
|
+
|
|
79
|
+
def add_exporter(self, name: str, exporter: BaseExporter) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Add an exporter to the manager.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
name (str): The unique name for the exporter.
|
|
85
|
+
exporter (BaseExporter): The exporter instance to add.
|
|
86
|
+
"""
|
|
87
|
+
self._ensure_registry_owned()
|
|
88
|
+
|
|
89
|
+
if name in self._exporter_registry:
|
|
90
|
+
logger.warning("Exporter '%s' already registered. Overwriting.", name)
|
|
91
|
+
|
|
92
|
+
self._exporter_registry[name] = exporter
|
|
93
|
+
|
|
94
|
+
def remove_exporter(self, name: str) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Remove an exporter from the manager.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
name (str): The name of the exporter to remove.
|
|
100
|
+
"""
|
|
101
|
+
self._ensure_registry_owned()
|
|
102
|
+
if name in self._exporter_registry:
|
|
103
|
+
del self._exporter_registry[name]
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"Cannot remove exporter '{name}' because it is not registered.")
|
|
106
|
+
|
|
107
|
+
def get_exporter(self, name: str) -> BaseExporter:
|
|
108
|
+
"""
|
|
109
|
+
Get an exporter instance by name.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
name (str): The name of the exporter to retrieve.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
BaseExporter: The exporter instance if found, otherwise raises a ValueError.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If the exporter is not found.
|
|
119
|
+
"""
|
|
120
|
+
exporter = self._exporter_registry.get(name, None)
|
|
121
|
+
|
|
122
|
+
if exporter is not None:
|
|
123
|
+
return exporter
|
|
124
|
+
|
|
125
|
+
raise ValueError(f"Cannot get exporter '{name}' because it is not registered.")
|
|
126
|
+
|
|
127
|
+
async def get_all_exporters(self) -> dict[str, BaseExporter]:
|
|
128
|
+
"""
|
|
129
|
+
Get all registered exporters instances.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
dict[str, BaseExporter]: A dictionary mapping exporter names to exporter instances.
|
|
133
|
+
"""
|
|
134
|
+
return self._exporter_registry
|
|
135
|
+
|
|
136
|
+
def create_isolated_exporters(self, context_state: ContextState | None = None) -> dict[str, BaseExporter]:
|
|
137
|
+
"""
|
|
138
|
+
Create isolated copies of all exporters for concurrent execution.
|
|
139
|
+
|
|
140
|
+
This uses copy-on-write to efficiently create isolated instances that share
|
|
141
|
+
expensive resources but have separate mutable state.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
context_state (ContextState | None, optional): The isolated context state for the new exporter instances.
|
|
145
|
+
If not provided, a new context state will be created.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict[str, BaseExporter]: Dictionary of isolated exporter instances
|
|
149
|
+
"""
|
|
150
|
+
# Provide default context state if None
|
|
151
|
+
if context_state is None:
|
|
152
|
+
context_state = ContextState.get()
|
|
153
|
+
|
|
154
|
+
isolated_exporters = {}
|
|
155
|
+
for name, exporter in self._exporter_registry.items():
|
|
156
|
+
if hasattr(exporter, 'create_isolated_instance'):
|
|
157
|
+
isolated_exporters[name] = exporter.create_isolated_instance(context_state)
|
|
158
|
+
else:
|
|
159
|
+
# Fallback for exporters that don't support isolation
|
|
160
|
+
logger.warning("Exporter '%s' doesn't support isolation, using shared instance", name)
|
|
161
|
+
isolated_exporters[name] = exporter
|
|
162
|
+
return isolated_exporters
|
|
163
|
+
|
|
164
|
+
async def _cleanup_isolated_exporters(self):
|
|
165
|
+
"""Explicitly clean up isolated exporter instances."""
|
|
166
|
+
if not self._active_isolated_exporters:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
logger.debug("Cleaning up %d isolated exporters", len(self._active_isolated_exporters))
|
|
170
|
+
|
|
171
|
+
cleanup_tasks = []
|
|
172
|
+
for name, exporter in self._active_isolated_exporters.items():
|
|
173
|
+
try:
|
|
174
|
+
# Only clean up isolated instances that have a stop method
|
|
175
|
+
if hasattr(exporter, 'stop') and exporter.is_isolated_instance:
|
|
176
|
+
cleanup_tasks.append(self._cleanup_single_exporter(name, exporter))
|
|
177
|
+
else:
|
|
178
|
+
logger.debug("Skipping cleanup for non-isolated exporter '%s'", name)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error("Error preparing cleanup for isolated exporter '%s': %s", name, e)
|
|
181
|
+
|
|
182
|
+
if cleanup_tasks:
|
|
183
|
+
# Run cleanup tasks concurrently with timeout
|
|
184
|
+
try:
|
|
185
|
+
await asyncio.wait_for(asyncio.gather(*cleanup_tasks, return_exceptions=True),
|
|
186
|
+
timeout=self._shutdown_timeout)
|
|
187
|
+
except asyncio.TimeoutError:
|
|
188
|
+
logger.warning("Some isolated exporters did not clean up within timeout")
|
|
189
|
+
|
|
190
|
+
self._active_isolated_exporters.clear()
|
|
191
|
+
|
|
192
|
+
async def _cleanup_single_exporter(self, name: str, exporter: BaseExporter):
|
|
193
|
+
"""Clean up a single isolated exporter."""
|
|
194
|
+
try:
|
|
195
|
+
logger.debug("Stopping isolated exporter '%s'", name)
|
|
196
|
+
await exporter.stop()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error("Error stopping isolated exporter '%s': %s", name, e)
|
|
199
|
+
|
|
200
|
+
@asynccontextmanager
|
|
201
|
+
async def start(self, context_state: ContextState | None = None):
|
|
202
|
+
"""
|
|
203
|
+
Start all registered exporters concurrently.
|
|
204
|
+
|
|
205
|
+
This method acquires a lock to ensure only one start/stop cycle is active at a time. It starts all
|
|
206
|
+
currently registered exporters in their own asyncio tasks. Exporters added after this call will not be
|
|
207
|
+
started until the next lifecycle.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
context_state: Optional context state for creating isolated exporters
|
|
211
|
+
|
|
212
|
+
Yields:
|
|
213
|
+
ExporterManager: The manager instance for use within the context.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
RuntimeError: If the manager is already running.
|
|
217
|
+
"""
|
|
218
|
+
async with self._lock:
|
|
219
|
+
if self._running:
|
|
220
|
+
raise RuntimeError("Exporter manager is already running")
|
|
221
|
+
self._shutdown_event.clear()
|
|
222
|
+
self._running = True
|
|
223
|
+
|
|
224
|
+
# Create isolated exporters if context_state provided, otherwise use originals
|
|
225
|
+
if context_state:
|
|
226
|
+
exporters_to_start = self.create_isolated_exporters(context_state)
|
|
227
|
+
# Store isolated exporters for cleanup
|
|
228
|
+
self._active_isolated_exporters = exporters_to_start
|
|
229
|
+
logger.debug("Created %d isolated exporters", len(exporters_to_start))
|
|
230
|
+
else:
|
|
231
|
+
exporters_to_start = self._exporter_registry
|
|
232
|
+
# Clear isolated exporters since we're using originals
|
|
233
|
+
self._active_isolated_exporters = {}
|
|
234
|
+
|
|
235
|
+
# Start all exporters concurrently
|
|
236
|
+
exporters = []
|
|
237
|
+
tasks = []
|
|
238
|
+
for name, exporter in exporters_to_start.items():
|
|
239
|
+
task = asyncio.create_task(self._run_exporter(name, exporter))
|
|
240
|
+
exporters.append(exporter)
|
|
241
|
+
self._tasks[name] = task
|
|
242
|
+
tasks.append(task)
|
|
243
|
+
|
|
244
|
+
# Wait for all exporters to be ready
|
|
245
|
+
await asyncio.gather(*[exporter.wait_ready() for exporter in exporters])
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
yield self
|
|
249
|
+
finally:
|
|
250
|
+
# Clean up isolated exporters BEFORE stopping tasks
|
|
251
|
+
try:
|
|
252
|
+
await self._cleanup_isolated_exporters()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error("Error during isolated exporter cleanup: %s", e)
|
|
255
|
+
|
|
256
|
+
# Then stop the manager tasks
|
|
257
|
+
await self.stop()
|
|
258
|
+
|
|
259
|
+
async def _run_exporter(self, name: str, exporter: BaseExporter):
|
|
260
|
+
"""
|
|
261
|
+
Run an exporter in its own task.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name (str): The name of the exporter.
|
|
265
|
+
exporter (BaseExporter): The exporter instance to run.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
async with exporter.start():
|
|
269
|
+
logger.info("Started exporter '%s'", name)
|
|
270
|
+
# The context manager will keep the task alive until shutdown is signaled
|
|
271
|
+
await self._shutdown_event.wait()
|
|
272
|
+
logger.info("Stopped exporter '%s'", name)
|
|
273
|
+
except asyncio.CancelledError:
|
|
274
|
+
logger.debug("Exporter '%s' task cancelled", name)
|
|
275
|
+
logger.info("Stopped exporter '%s'", name)
|
|
276
|
+
raise
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error("Failed to run exporter '%s': %s", name, str(e), exc_info=True)
|
|
279
|
+
# Re-raise the exception to ensure it's properly handled
|
|
280
|
+
raise
|
|
281
|
+
|
|
282
|
+
async def stop(self) -> None:
|
|
283
|
+
"""
|
|
284
|
+
Stop all registered exporters.
|
|
285
|
+
|
|
286
|
+
This method signals all running exporter tasks to shut down and waits for their completion, up to the
|
|
287
|
+
configured shutdown timeout. If any tasks do not complete in time, a warning is logged.
|
|
288
|
+
"""
|
|
289
|
+
async with self._lock:
|
|
290
|
+
if not self._running:
|
|
291
|
+
return
|
|
292
|
+
self._running = False
|
|
293
|
+
self._shutdown_event.set()
|
|
294
|
+
|
|
295
|
+
# Create a copy of tasks to prevent modification during iteration
|
|
296
|
+
tasks_to_cancel = dict(self._tasks)
|
|
297
|
+
self._tasks.clear()
|
|
298
|
+
stuck_tasks = []
|
|
299
|
+
# Cancel all running tasks and await their completion
|
|
300
|
+
for name, task in tasks_to_cancel.items():
|
|
301
|
+
try:
|
|
302
|
+
task.cancel()
|
|
303
|
+
await asyncio.wait_for(task, timeout=self._shutdown_timeout)
|
|
304
|
+
except asyncio.TimeoutError:
|
|
305
|
+
logger.warning("Exporter '%s' task did not shut down in time and may be stuck.", name)
|
|
306
|
+
stuck_tasks.append(name)
|
|
307
|
+
except asyncio.CancelledError:
|
|
308
|
+
logger.debug("Exporter '%s' task cancelled", name)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error("Failed to stop exporter '%s': %s", name, str(e))
|
|
311
|
+
|
|
312
|
+
if stuck_tasks:
|
|
313
|
+
logger.warning("Exporters did not shut down in time: %s", ", ".join(stuck_tasks))
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def from_exporters(exporters: dict[str, BaseExporter], shutdown_timeout: int = 120) -> "ExporterManager":
|
|
317
|
+
"""
|
|
318
|
+
Create an ExporterManager from a dictionary of exporters.
|
|
319
|
+
"""
|
|
320
|
+
exporter_manager = ExporterManager(shutdown_timeout=shutdown_timeout)
|
|
321
|
+
for name, exporter in exporters.items():
|
|
322
|
+
exporter_manager.add_exporter(name, exporter)
|
|
323
|
+
|
|
324
|
+
return exporter_manager
|
|
325
|
+
|
|
326
|
+
def get(self) -> "ExporterManager":
|
|
327
|
+
"""
|
|
328
|
+
Create a copy of this ExporterManager with the same configuration using copy-on-write.
|
|
329
|
+
|
|
330
|
+
This is the most efficient approach - shares the registry until modifications are needed.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
ExporterManager: A new ExporterManager instance with shared exporters (copy-on-write).
|
|
334
|
+
"""
|
|
335
|
+
return self._create_with_shared_registry(self._shutdown_timeout, self._exporter_registry)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BatchConfigMixin(BaseModel):
|
|
21
|
+
"""Mixin for telemetry exporters that require batching."""
|
|
22
|
+
batch_size: int = Field(default=100, description="The batch size for the telemetry exporter.")
|
|
23
|
+
flush_interval: float = Field(default=5.0, description="The flush interval for the telemetry exporter.")
|
|
24
|
+
max_queue_size: int = Field(default=1000, description="The maximum queue size for the telemetry exporter.")
|
|
25
|
+
drop_on_overflow: bool = Field(default=False, description="Whether to drop on overflow for the telemetry exporter.")
|
|
26
|
+
shutdown_timeout: float = Field(default=10.0, description="The shutdown timeout for the telemetry exporter.")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CollectorConfigMixin(BaseModel):
|
|
21
|
+
"""Mixin for telemetry exporters that require a project name and endpoint when exporting to a collector service."""
|
|
22
|
+
project: str = Field(description="The project name to associate the telemetry traces.")
|
|
23
|
+
endpoint: str = Field(description="The endpoint of the telemetry collector service.")
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from nat.observability.mixin.file_mode import FileMode
|
|
23
|
+
from nat.observability.mixin.resource_conflict_mixin import ResourceConflictMixin
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FileExportMixin(ResourceConflictMixin):
|
|
29
|
+
"""Mixin for file-based exporters.
|
|
30
|
+
|
|
31
|
+
This mixin provides file I/O functionality for exporters that need to write
|
|
32
|
+
serialized data to local files, with support for file overwriting and rolling logs.
|
|
33
|
+
|
|
34
|
+
Automatically detects and prevents file path conflicts between multiple instances
|
|
35
|
+
by raising ResourceConflictError during initialization.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*args,
|
|
41
|
+
output_path,
|
|
42
|
+
project,
|
|
43
|
+
mode: FileMode = FileMode.APPEND,
|
|
44
|
+
enable_rolling: bool = False,
|
|
45
|
+
max_file_size: int = 10 * 1024 * 1024, # 10MB default
|
|
46
|
+
max_files: int = 5,
|
|
47
|
+
cleanup_on_init: bool = False,
|
|
48
|
+
**kwargs):
|
|
49
|
+
"""Initialize the file exporter with the specified output_path and project.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
output_path (str): The path to the output file or directory (if rolling enabled).
|
|
53
|
+
project (str): The project name for metadata.
|
|
54
|
+
mode (str): Either "append" or "overwrite". Defaults to "append".
|
|
55
|
+
enable_rolling (bool): Enable rolling log files. Defaults to False.
|
|
56
|
+
max_file_size (int): Maximum file size in bytes before rolling. Defaults to 10MB.
|
|
57
|
+
max_files (int): Maximum number of rolled files to keep. Defaults to 5.
|
|
58
|
+
cleanup_on_init (bool): Clean up old files during initialization. Defaults to False.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ResourceConflictError: If another FileExportMixin instance is already using
|
|
62
|
+
the same file path or would create conflicting files.
|
|
63
|
+
"""
|
|
64
|
+
self._filepath = Path(output_path)
|
|
65
|
+
self._project = project
|
|
66
|
+
self._mode = mode
|
|
67
|
+
self._enable_rolling = enable_rolling
|
|
68
|
+
self._max_file_size = max_file_size
|
|
69
|
+
self._max_files = max_files
|
|
70
|
+
self._cleanup_on_init = cleanup_on_init
|
|
71
|
+
self._lock = asyncio.Lock()
|
|
72
|
+
self._first_write = True
|
|
73
|
+
|
|
74
|
+
# Initialize file paths first, then check for conflicts via ResourceConflictMixin
|
|
75
|
+
self._setup_file_paths()
|
|
76
|
+
|
|
77
|
+
# This calls _register_resources() which will check for conflicts
|
|
78
|
+
super().__init__(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
def _setup_file_paths(self):
|
|
81
|
+
"""Setup file paths using the project name."""
|
|
82
|
+
|
|
83
|
+
if self._enable_rolling:
|
|
84
|
+
# If rolling is enabled, output_path should be a directory
|
|
85
|
+
self._base_dir = self._filepath if self._filepath.is_dir(
|
|
86
|
+
) or not self._filepath.suffix else self._filepath.parent
|
|
87
|
+
self._base_filename = self._filepath.stem if self._filepath.suffix else f"{self._project}_export"
|
|
88
|
+
self._file_extension = self._filepath.suffix or ".log"
|
|
89
|
+
self._base_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
self._current_file_path = self._base_dir / f"{self._base_filename}{self._file_extension}"
|
|
91
|
+
|
|
92
|
+
# Perform initial cleanup if requested
|
|
93
|
+
if self._cleanup_on_init:
|
|
94
|
+
self._cleanup_old_files_sync()
|
|
95
|
+
else:
|
|
96
|
+
# Traditional single file mode
|
|
97
|
+
self._filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
self._current_file_path = self._filepath
|
|
99
|
+
|
|
100
|
+
# For single file mode with overwrite, remove existing file
|
|
101
|
+
if self._mode == FileMode.OVERWRITE and self._cleanup_on_init and self._current_file_path.exists():
|
|
102
|
+
try:
|
|
103
|
+
self._current_file_path.unlink()
|
|
104
|
+
logger.info("Cleaned up existing file: %s", self._current_file_path)
|
|
105
|
+
except OSError as e:
|
|
106
|
+
logger.error("Error removing existing file %s: %s", self._current_file_path, e)
|
|
107
|
+
|
|
108
|
+
def _get_resource_identifiers(self) -> dict[str, Any]:
|
|
109
|
+
"""Return the file resources this instance will use.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
dict with file_path and optionally cleanup_pattern for rolling files.
|
|
113
|
+
"""
|
|
114
|
+
identifiers = {"file_path": str(self._current_file_path.resolve())}
|
|
115
|
+
|
|
116
|
+
# Add cleanup pattern for rolling files
|
|
117
|
+
if self._enable_rolling:
|
|
118
|
+
cleanup_pattern = f"{self._base_filename}_*{self._file_extension}"
|
|
119
|
+
pattern_key = f"{self._base_dir.resolve()}:{cleanup_pattern}"
|
|
120
|
+
identifiers["cleanup_pattern"] = pattern_key
|
|
121
|
+
|
|
122
|
+
return identifiers
|
|
123
|
+
|
|
124
|
+
def _format_conflict_error(self, resource_type: str, identifier: Any, existing_instance: Any) -> str:
|
|
125
|
+
"""Format user-friendly error messages for file conflicts."""
|
|
126
|
+
match resource_type:
|
|
127
|
+
case "file_path":
|
|
128
|
+
return (f"File path conflict detected: '{self._current_file_path}' is already in use by another "
|
|
129
|
+
f"FileExportMixin instance (project: '{existing_instance._project}'). "
|
|
130
|
+
f"Use different project names or output paths to avoid conflicts.")
|
|
131
|
+
case "cleanup_pattern":
|
|
132
|
+
return (f"Rolling file cleanup conflict detected: Both instances would use pattern "
|
|
133
|
+
f"'{self._base_filename}_*{self._file_extension}' in directory '{self._base_dir}', "
|
|
134
|
+
f"causing one to delete the other's files. "
|
|
135
|
+
f"Current instance (project: '{self._project}'), "
|
|
136
|
+
f"existing instance (project: '{existing_instance._project}'). "
|
|
137
|
+
f"Use different project names or directories to avoid conflicts.")
|
|
138
|
+
case _:
|
|
139
|
+
return f"Unknown file resource conflict: {resource_type} = {identifier}"
|
|
140
|
+
|
|
141
|
+
def _cleanup_old_files_sync(self) -> None:
|
|
142
|
+
"""Synchronous version of cleanup for use during initialization."""
|
|
143
|
+
try:
|
|
144
|
+
# Find all rolled files matching our pattern
|
|
145
|
+
pattern = f"{self._base_filename}_*{self._file_extension}"
|
|
146
|
+
rolled_files = list(self._base_dir.glob(pattern))
|
|
147
|
+
|
|
148
|
+
# Sort by modification time (newest first)
|
|
149
|
+
rolled_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
|
|
150
|
+
|
|
151
|
+
# Remove files beyond max_files limit
|
|
152
|
+
for old_file in rolled_files[self._max_files:]:
|
|
153
|
+
try:
|
|
154
|
+
old_file.unlink()
|
|
155
|
+
logger.info("Cleaned up old log file during init: %s", old_file)
|
|
156
|
+
except OSError as e:
|
|
157
|
+
logger.error("Error removing old file %s: %s", old_file, e)
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error("Error during initialization cleanup: %s", e)
|
|
161
|
+
|
|
162
|
+
async def _should_roll_file(self) -> bool:
|
|
163
|
+
"""Check if the current file should be rolled based on size."""
|
|
164
|
+
if not self._enable_rolling:
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
if self._current_file_path.exists():
|
|
169
|
+
stat = self._current_file_path.stat()
|
|
170
|
+
return stat.st_size >= self._max_file_size
|
|
171
|
+
except OSError:
|
|
172
|
+
pass
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
async def _roll_file(self) -> None:
|
|
176
|
+
"""Roll the current file by renaming it with a timestamp and cleaning up old files."""
|
|
177
|
+
if not self._current_file_path.exists():
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# Generate timestamped filename with microsecond precision
|
|
181
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
182
|
+
rolled_filename = f"{self._base_filename}_{timestamp}{self._file_extension}"
|
|
183
|
+
rolled_path = self._base_dir / rolled_filename
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Rename current file
|
|
187
|
+
self._current_file_path.rename(rolled_path)
|
|
188
|
+
logger.info("Rolled log file to: %s", rolled_path)
|
|
189
|
+
|
|
190
|
+
# Clean up old files
|
|
191
|
+
await self._cleanup_old_files()
|
|
192
|
+
|
|
193
|
+
except OSError as e:
|
|
194
|
+
logger.error("Error rolling file %s: %s", self._current_file_path, e)
|
|
195
|
+
|
|
196
|
+
async def _cleanup_old_files(self) -> None:
|
|
197
|
+
"""Remove old rolled files beyond the maximum count."""
|
|
198
|
+
try:
|
|
199
|
+
# Find all rolled files matching our pattern
|
|
200
|
+
pattern = f"{self._base_filename}_*{self._file_extension}"
|
|
201
|
+
rolled_files = list(self._base_dir.glob(pattern))
|
|
202
|
+
|
|
203
|
+
# Sort by modification time (newest first)
|
|
204
|
+
rolled_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
|
|
205
|
+
|
|
206
|
+
# Remove files beyond max_files limit
|
|
207
|
+
for old_file in rolled_files[self._max_files:]:
|
|
208
|
+
try:
|
|
209
|
+
old_file.unlink()
|
|
210
|
+
logger.info("Cleaned up old log file: %s", old_file)
|
|
211
|
+
except OSError as e:
|
|
212
|
+
logger.error("Error removing old file %s: %s", old_file, e)
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error("Error during cleanup: %s", e)
|
|
216
|
+
|
|
217
|
+
async def export_processed(self, item: str | list[str]) -> None:
|
|
218
|
+
"""Export a processed string or list of strings.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
item (str | list[str]): The string or list of strings to export.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
# Lazy import to avoid slow startup times
|
|
225
|
+
import aiofiles
|
|
226
|
+
|
|
227
|
+
async with self._lock:
|
|
228
|
+
# Check if we need to roll the file
|
|
229
|
+
if await self._should_roll_file():
|
|
230
|
+
await self._roll_file()
|
|
231
|
+
|
|
232
|
+
# Determine file mode
|
|
233
|
+
if self._first_write and self._mode == FileMode.OVERWRITE:
|
|
234
|
+
file_mode = "w"
|
|
235
|
+
self._first_write = False
|
|
236
|
+
else:
|
|
237
|
+
file_mode = "a"
|
|
238
|
+
|
|
239
|
+
async with aiofiles.open(self._current_file_path, mode=file_mode) as f:
|
|
240
|
+
if isinstance(item, list):
|
|
241
|
+
# Handle list of strings
|
|
242
|
+
for single_item in item:
|
|
243
|
+
await f.write(single_item)
|
|
244
|
+
await f.write("\n")
|
|
245
|
+
else:
|
|
246
|
+
# Handle single string
|
|
247
|
+
await f.write(item)
|
|
248
|
+
await f.write("\n")
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.error("Error exporting event: %s", e, exc_info=True)
|
|
252
|
+
|
|
253
|
+
def get_current_file_path(self) -> Path:
|
|
254
|
+
"""Get the current file path being written to.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Path: The current file path being written to.
|
|
258
|
+
"""
|
|
259
|
+
return self._current_file_path
|
|
260
|
+
|
|
261
|
+
def get_file_info(self) -> dict:
|
|
262
|
+
"""Get information about the current file and rolling configuration.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
dict: A dictionary containing the current file path, mode, rolling enabled, cleanup on init,
|
|
266
|
+
effective project name, and additional rolling configuration if enabled.
|
|
267
|
+
"""
|
|
268
|
+
info = {
|
|
269
|
+
"current_file": str(self._current_file_path),
|
|
270
|
+
"mode": self._mode,
|
|
271
|
+
"rolling_enabled": self._enable_rolling,
|
|
272
|
+
"cleanup_on_init": self._cleanup_on_init,
|
|
273
|
+
"project": self._project,
|
|
274
|
+
"effective_project": self._project,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if self._enable_rolling:
|
|
278
|
+
info.update({
|
|
279
|
+
"max_file_size": self._max_file_size,
|
|
280
|
+
"max_files": self._max_files,
|
|
281
|
+
"base_directory": str(self._base_dir),
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
# Add current file size if it exists
|
|
285
|
+
if self._current_file_path.exists():
|
|
286
|
+
info["current_file_size"] = self._current_file_path.stat().st_size
|
|
287
|
+
|
|
288
|
+
return info
|