digitalkin 1.0.0.dev19__tar.gz → 1.0.0.dev21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/PKG-INFO +1 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/pyproject.toml +1 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/__version__.py +1 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/community/agno/hitl.py +1 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/exceptions.py +4 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/gateway_servicer.py +139 -78
- digitalkin-1.0.0.dev21/src/digitalkin/grpc_servers/interceptors/permission.py +103 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +2 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/utils/grpc_error_handler.py +4 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/gateway.py +22 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/_base_module.py +35 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/setup/grpc_setup.py +4 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/storage/grpc_storage.py +41 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin.egg-info/PKG-INFO +1 -1
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin.egg-info/SOURCES.txt +1 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/LICENSE +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/README.md +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/base_server/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/base_server/mock/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/base_server/mock/mock_pb2.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/base_server/server_async_insecure.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/base_server/server_async_secure.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/base_server/server_sync_insecure.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/base_server/server_sync_secure.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/echo_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/models/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/models/input.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/models/output.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/models/secret.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/models/setup.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/server.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/triggers/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/bench_module/triggers/message_trigger.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/modules/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/modules/archetype_with_tools_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/modules/cpu_intensive_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/modules/dynamic_setup_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/modules/minimal_llm_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/modules/text_transform_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/monitoring/digitalkin_observability/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/monitoring/digitalkin_observability/http_server.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/monitoring/digitalkin_observability/interceptors.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/monitoring/digitalkin_observability/metrics.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/monitoring/digitalkin_observability/prometheus.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/monitoring/tests/test_metrics.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/client.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/echo_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/models/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/models/input.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/models/output.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/models/secret.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/models/setup.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/server.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/triggers/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/redis_demo/triggers/message_trigger.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/services/filesystem_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/examples/services/storage_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/setup.cfg +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/community/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/community/agno/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/community/agno/agno_adapter.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/community/agno/agui_tools.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/community/agno/models.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/common/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/common/factories.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/job_manager/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/job_manager/base_job_manager.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/job_manager/single_job_manager.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/profiling/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/profiling/step_timer.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/profiling/task_profiler.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/resilience/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/resilience/bulkhead.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/resilience/task_supervisor.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/base_task_manager.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/local_task_manager.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/module_runner.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/redis/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/redis/proto_streams.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/redis/redis_client.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/redis/redis_idempotency.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/redis/redis_signal.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/redis/redis_state.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/remote_task_manager.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/task_executor.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/core/task_manager/task_session.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/_base_server.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/interceptors/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/interceptors/request_ids.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/m2m_call_registry.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/module_server.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/module_servicer.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/stream_registry.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/stream_session.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/utils/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/utils/circuit_breaker.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/utils/utility_schema_extender.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/utils/validators.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/logger.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/mixins/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/mixins/agui_mixin.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/mixins/base_mixin.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/mixins/cost_mixin.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/mixins/file_history_mixin.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/mixins/filesystem_mixin.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/mixins/logger_mixin.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/mixins/storage_mixin.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/core/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/core/job_manager_models.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/core/redis.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/core/task_monitor.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/events/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/events/agent_events.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/grpc_servers/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/grpc_servers/circuit_breaker.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/grpc_servers/m2m.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/grpc_servers/models.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/grpc_servers/stream_error_codes.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/grpc_servers/types.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/ag_ui.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/base_types.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/module_context.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/module_types.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/request_metadata.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/select_schema.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/setup_types.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/tool_cache.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/tool_reference.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/module/utility.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/services/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/services/cost.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/services/registry.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/services/services.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/services/storage.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/grpc_client.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/log.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/profiling.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/queue.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/redis.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/resilience.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/server/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/server/channel.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/server/grpc.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/server/server.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/server/servicer.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/task_manager.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/utils/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/settings/utils/channel.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/utils/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/models/utils/dynamic_schema.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/archetype_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/tool_module.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/trigger_handler.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/triggers/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/triggers/healthcheck_ping_trigger.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/triggers/healthcheck_services_trigger.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/modules/triggers/healthcheck_status_trigger.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/py.typed +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/base_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/communication/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/communication/communication_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/communication/default_communication.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/communication/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/communication/grpc_communication.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/cost/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/cost/cost_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/cost/default_cost.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/cost/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/cost/grpc_cost.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/filesystem/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/filesystem/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/identity/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/identity/default_identity.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/identity/identity_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/registry/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/registry/default_registry.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/registry/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/registry/grpc_registry.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/registry/registry_models.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/registry/registry_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/services_config.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/services_models.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/setup/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/setup/default_setup.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/setup/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/setup/setup_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/storage/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/storage/default_storage.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/storage/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/storage/storage_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/task_manager/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/task_manager/default_task_manager.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/task_manager/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/task_manager/redis_task_manager.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/task_manager/task_manager_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/user_profile/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/user_profile/default_user_profile.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/user_profile/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/user_profile/grpc_user_profile.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/services/user_profile/user_profile_strategy.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/__init__.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/arg_parser.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/conditional_schema.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/development_mode_action.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/dynamic_schema.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/exceptions.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/llm_ready_schema.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/package_discover.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/proto_utils.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/utils/schema_splitter.py +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin.egg-info/dependency_links.txt +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin.egg-info/requires.txt +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin.egg-info/top_level.txt +0 -0
- {digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/tests/test_exceptions.py +0 -0
|
@@ -611,7 +611,7 @@ class AgnoHitlRunner:
|
|
|
611
611
|
stream: Any,
|
|
612
612
|
send: Callable[[BaseAgentRunEvent], Coroutine[Any, Any, None]],
|
|
613
613
|
thread_id: str,
|
|
614
|
-
run_output_cls: type,
|
|
614
|
+
run_output_cls: type[RunOutput],
|
|
615
615
|
) -> PauseInfo | None:
|
|
616
616
|
"""Drain an Agno stream, forward events, persist on pause.
|
|
617
617
|
|
|
@@ -19,6 +19,10 @@ class SecurityError(ServerError):
|
|
|
19
19
|
"""Error related to security configuration."""
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class PermissionDeniedError(ServerError):
|
|
23
|
+
"""Remote service rejected the call with gRPC PERMISSION_DENIED."""
|
|
24
|
+
|
|
25
|
+
|
|
22
26
|
class ServerStateError(ServerError):
|
|
23
27
|
"""Error related to server state (e.g., already started, not started)."""
|
|
24
28
|
|
{digitalkin-1.0.0.dev19 → digitalkin-1.0.0.dev21}/src/digitalkin/grpc_servers/gateway_servicer.py
RENAMED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import contextlib
|
|
7
7
|
import json
|
|
8
|
+
import random
|
|
8
9
|
import time
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
from typing import TYPE_CHECKING, Any
|
|
@@ -34,7 +35,7 @@ from digitalkin.services.communication.exceptions import InvalidConsumerAddressE
|
|
|
34
35
|
from digitalkin.services.communication.grpc_communication import GrpcCommunication
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
37
|
-
from collections.abc import AsyncGenerator, AsyncIterator
|
|
38
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Callable
|
|
38
39
|
|
|
39
40
|
from digitalkin.core.task_manager.module_runner import ModuleRunner
|
|
40
41
|
from digitalkin.core.task_manager.redis.redis_client import RedisClient
|
|
@@ -157,7 +158,7 @@ class GatewayServicer:
|
|
|
157
158
|
await self._m2m.stop()
|
|
158
159
|
await self._registry.shutdown()
|
|
159
160
|
|
|
160
|
-
async def StartStream( # noqa:
|
|
161
|
+
async def StartStream( # noqa: PLR0911, PLR0915
|
|
161
162
|
self,
|
|
162
163
|
request: Any,
|
|
163
164
|
context: grpc.aio.ServicerContext,
|
|
@@ -208,13 +209,18 @@ class GatewayServicer:
|
|
|
208
209
|
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
209
210
|
|
|
210
211
|
if self._registry.get(task_id) is not None:
|
|
211
|
-
logger.
|
|
212
|
-
|
|
212
|
+
logger.info(
|
|
213
|
+
"[VALIDATE RD1] StartStream refused: a dial is already live for task_id=%s",
|
|
214
|
+
task_id,
|
|
215
|
+
extra=log_extra,
|
|
216
|
+
) # TODO(validate): remove after prod validation
|
|
217
|
+
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
213
218
|
timer.mark("dedup_check")
|
|
214
219
|
|
|
215
220
|
# Durable at-most-once guard: survives session teardown and spans replicas.
|
|
216
|
-
#
|
|
217
|
-
#
|
|
221
|
+
# Reconnection is server-driven (the live dial-back auto re-dials the same
|
|
222
|
+
# consumer), so a re-issued StartStream for an already-claimed/running task is
|
|
223
|
+
# REFUSED — one task_id maps to exactly one dial.
|
|
218
224
|
try:
|
|
219
225
|
claim = await self._idempotency.claim(task_id, SharedRedisListener.PROCESS_ID)
|
|
220
226
|
except RedisError:
|
|
@@ -226,59 +232,13 @@ class GatewayServicer:
|
|
|
226
232
|
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
227
233
|
timer.mark("idempotency_claim")
|
|
228
234
|
if claim is not ClaimResult.CLAIMED:
|
|
229
|
-
# Re-issued StartStream for an already-claimed task. The prior dial-back
|
|
230
|
-
# is dead (the dedup branch above returned if its session were still live),
|
|
231
|
-
# so re-dial in resume mode to re-attach the restarted consumer from its
|
|
232
|
-
# cursor — as long as the durable stream is still retained.
|
|
233
|
-
try:
|
|
234
|
-
stream_len = await self._redis_client.xlen(f"task:{task_id}:stream")
|
|
235
|
-
except RedisError:
|
|
236
|
-
logger.warning(
|
|
237
|
-
"[VALIDATE R2] StartStream xlen Redis failure → accepted=False",
|
|
238
|
-
extra=log_extra,
|
|
239
|
-
exc_info=True,
|
|
240
|
-
) # TODO(validate): remove after prod validation
|
|
241
|
-
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
242
|
-
if stream_len == 0:
|
|
243
|
-
logger.info(
|
|
244
|
-
"[VALIDATE IDEM1] no live stream to resume (TTL expired) task_id=%s (%s)",
|
|
245
|
-
task_id,
|
|
246
|
-
claim.name,
|
|
247
|
-
extra=log_extra,
|
|
248
|
-
) # TODO(validate): remove after prod validation
|
|
249
|
-
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
250
|
-
# Atomic guard: recheck + register + spawn with no await that yields
|
|
251
|
-
# between them, so a concurrent re-poke falls into the dedup branch and acks.
|
|
252
|
-
if self._registry.get(task_id) is not None:
|
|
253
|
-
logger.debug("resume-dial already scheduled, reusing", extra=log_extra)
|
|
254
|
-
return gateway_pb2.StartStreamResponse(accepted=True, task_id=task_id)
|
|
255
|
-
session = StreamSession(task_id=task_id)
|
|
256
|
-
registered = await self._registry.register(
|
|
257
|
-
session,
|
|
258
|
-
setup_id=request.setup_id,
|
|
259
|
-
mission_id=request.mission_id,
|
|
260
|
-
)
|
|
261
|
-
if not registered:
|
|
262
|
-
logger.warning("Resume-dial rejected (capacity)", extra=log_extra)
|
|
263
|
-
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
264
235
|
logger.info(
|
|
265
|
-
"[VALIDATE
|
|
236
|
+
"[VALIDATE RD1] StartStream refused: task already claimed/running task_id=%s (%s)",
|
|
266
237
|
task_id,
|
|
267
238
|
claim.name,
|
|
268
|
-
stream_len,
|
|
269
239
|
extra=log_extra,
|
|
270
240
|
) # TODO(validate): remove after prod validation
|
|
271
|
-
|
|
272
|
-
self._dial_consumer(
|
|
273
|
-
task_id=task_id,
|
|
274
|
-
mission_id=request.mission_id,
|
|
275
|
-
setup_id=request.setup_id,
|
|
276
|
-
address=client_address,
|
|
277
|
-
resume=True,
|
|
278
|
-
),
|
|
279
|
-
name=f"dial_consumer_{task_id}",
|
|
280
|
-
)
|
|
281
|
-
return gateway_pb2.StartStreamResponse(accepted=True, task_id=task_id)
|
|
241
|
+
return gateway_pb2.StartStreamResponse(accepted=False, task_id=task_id)
|
|
282
242
|
|
|
283
243
|
session = StreamSession(task_id=task_id)
|
|
284
244
|
accepted = await self._registry.register(
|
|
@@ -763,35 +723,133 @@ class GatewayServicer:
|
|
|
763
723
|
return
|
|
764
724
|
yield resp
|
|
765
725
|
|
|
766
|
-
async def _dial_consumer( # noqa: C901
|
|
726
|
+
async def _dial_consumer( # noqa: C901
|
|
767
727
|
self,
|
|
768
728
|
task_id: str,
|
|
769
729
|
mission_id: str,
|
|
770
730
|
setup_id: str,
|
|
771
731
|
address: str,
|
|
772
|
-
*,
|
|
773
|
-
resume: bool = False,
|
|
774
732
|
) -> None:
|
|
775
|
-
"""Dial the consumer's GatewayService.Stream
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
733
|
+
"""Dial the consumer's GatewayService.Stream, with server-side auto-reconnect.
|
|
734
|
+
|
|
735
|
+
First attempt is a fresh dial (``stream.init`` → consumer query →
|
|
736
|
+
``ModuleRunner`` → drain from seq 0). If the BiDi dies before the stream
|
|
737
|
+
is fully delivered and the module is still producing, re-dial the SAME
|
|
738
|
+
address in resume mode (``stream.resume`` → consumer replies with its
|
|
739
|
+
``from_seq`` → drain from that cursor, deduping the Redis queue) with
|
|
740
|
+
jittered backoff until the client returns or the reconnect window
|
|
741
|
+
(``DIGITALKIN_GATEWAY_DIAL_BACK_RECONNECT_WINDOW_S``) elapses. The module
|
|
742
|
+
runs as a separate task, spawned exactly once, so it keeps writing to
|
|
743
|
+
Redis across reconnects.
|
|
783
744
|
|
|
784
745
|
Args:
|
|
785
746
|
task_id: Task to push.
|
|
786
747
|
mission_id: Mission ID (logging context).
|
|
787
748
|
setup_id: Setup ID (logging context).
|
|
788
749
|
address: ``host:port`` of the consumer's GatewayService.
|
|
789
|
-
resume: Re-attach to an existing task's output instead of starting it.
|
|
790
750
|
"""
|
|
791
751
|
log_extra = {"task_id": task_id, "setup_id": setup_id, "mission_id": mission_id}
|
|
792
752
|
# Bind IDs so the dial-back BiDi + logs carry them (task-local spawned context).
|
|
793
753
|
RequestContext.bind(task_id=task_id, setup_id=setup_id, mission_id=mission_id)
|
|
794
754
|
|
|
755
|
+
module_spawned = False
|
|
756
|
+
|
|
757
|
+
def _mark_spawned() -> None:
|
|
758
|
+
nonlocal module_spawned
|
|
759
|
+
module_spawned = True
|
|
760
|
+
|
|
761
|
+
reconnect = get_gateway_settings().dial_reconnect
|
|
762
|
+
deadline: float | None = None
|
|
763
|
+
attempt = 0
|
|
764
|
+
resume = False
|
|
765
|
+
try:
|
|
766
|
+
while True:
|
|
767
|
+
disconnected = await self._run_dial_attempt(
|
|
768
|
+
task_id=task_id,
|
|
769
|
+
mission_id=mission_id,
|
|
770
|
+
setup_id=setup_id,
|
|
771
|
+
address=address,
|
|
772
|
+
resume=resume,
|
|
773
|
+
on_runner_spawn=_mark_spawned,
|
|
774
|
+
)
|
|
775
|
+
if not disconnected:
|
|
776
|
+
break
|
|
777
|
+
# Client BiDi died mid-stream. Re-dial only while there is a
|
|
778
|
+
# retained stream to resume and the reboot window hasn't elapsed.
|
|
779
|
+
if not module_spawned:
|
|
780
|
+
break
|
|
781
|
+
session = self._registry.get(task_id)
|
|
782
|
+
if session is None or session._stop_event.is_set(): # noqa: SLF001
|
|
783
|
+
break
|
|
784
|
+
try:
|
|
785
|
+
stream_len = await self._redis_client.xlen(f"task:{task_id}:stream")
|
|
786
|
+
except RedisError:
|
|
787
|
+
break
|
|
788
|
+
if stream_len == 0:
|
|
789
|
+
break
|
|
790
|
+
now = time.monotonic()
|
|
791
|
+
if deadline is None:
|
|
792
|
+
deadline = now + reconnect.window_s
|
|
793
|
+
if now >= deadline:
|
|
794
|
+
logger.info(
|
|
795
|
+
"[VALIDATE RD1] dial-back gave up after %.0fs reconnect window: task_id=%s attempts=%d",
|
|
796
|
+
reconnect.window_s,
|
|
797
|
+
task_id,
|
|
798
|
+
attempt,
|
|
799
|
+
) # TODO(validate): remove after prod validation
|
|
800
|
+
break
|
|
801
|
+
attempt += 1
|
|
802
|
+
delay = min(random.uniform(reconnect.backoff_base_s, reconnect.backoff_max_s), deadline - now) # noqa: S311
|
|
803
|
+
logger.info(
|
|
804
|
+
"[VALIDATE RD1] dial-back disconnected — re-dial attempt %d in %.1fs (resume): task_id=%s",
|
|
805
|
+
attempt,
|
|
806
|
+
delay,
|
|
807
|
+
task_id,
|
|
808
|
+
) # TODO(validate): remove after prod validation
|
|
809
|
+
await asyncio.sleep(max(0.0, delay))
|
|
810
|
+
resume = True
|
|
811
|
+
finally:
|
|
812
|
+
# End-of-stream cleanup (once). Output stream is left intact for replay.
|
|
813
|
+
try:
|
|
814
|
+
removed = await self._registry.unregister(task_id)
|
|
815
|
+
if removed is not None:
|
|
816
|
+
await removed.teardown()
|
|
817
|
+
except Exception:
|
|
818
|
+
logger.exception("end-of-stream unregister failed", extra=log_extra)
|
|
819
|
+
|
|
820
|
+
async def _run_dial_attempt( # noqa: C901, PLR0912, PLR0914, PLR0915
|
|
821
|
+
self,
|
|
822
|
+
*,
|
|
823
|
+
task_id: str,
|
|
824
|
+
mission_id: str,
|
|
825
|
+
setup_id: str,
|
|
826
|
+
address: str,
|
|
827
|
+
resume: bool,
|
|
828
|
+
on_runner_spawn: Callable[[], None],
|
|
829
|
+
) -> bool:
|
|
830
|
+
"""Run one dial-back BiDi attempt (fresh or resume).
|
|
831
|
+
|
|
832
|
+
Fresh (``resume=False``): sends ``stream.init``, spawns the
|
|
833
|
+
``ModuleRunner`` on the consumer's first reply (calling
|
|
834
|
+
``on_runner_spawn``), drains from seq 0. Resume: sends ``stream.resume``,
|
|
835
|
+
reads the consumer's cursor from the first reply's ``from_seq``, skips
|
|
836
|
+
the runner, drains from that cursor. Releases the channel before
|
|
837
|
+
returning; does NOT unregister the session (the caller owns lifecycle).
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
task_id: Task to push.
|
|
841
|
+
mission_id: Mission ID (logging context).
|
|
842
|
+
setup_id: Setup ID (logging context).
|
|
843
|
+
address: ``host:port`` of the consumer's GatewayService.
|
|
844
|
+
resume: Re-attach to an existing task's output instead of starting it.
|
|
845
|
+
on_runner_spawn: Called once, when this attempt spawns the module runner.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
True if the BiDi died before delivering ``stream.end`` (a re-dial
|
|
849
|
+
candidate); False on clean completion or a terminal failure.
|
|
850
|
+
"""
|
|
851
|
+
log_extra = {"task_id": task_id, "setup_id": setup_id, "mission_id": mission_id}
|
|
852
|
+
|
|
795
853
|
async def _fail(code: str, message: str) -> None:
|
|
796
854
|
"""Emit a fatal to Redis (fresh dial) or log only (resume).
|
|
797
855
|
|
|
@@ -816,7 +874,7 @@ class GatewayServicer:
|
|
|
816
874
|
StreamErrorCode.DIAL_BACK_INTERNAL.value,
|
|
817
875
|
"gateway has no client_config to dial back with",
|
|
818
876
|
)
|
|
819
|
-
return
|
|
877
|
+
return False
|
|
820
878
|
# setup_version_id is unused on the dial-back path.
|
|
821
879
|
comm = GrpcCommunication(
|
|
822
880
|
mission_id=mission_id,
|
|
@@ -838,14 +896,14 @@ class GatewayServicer:
|
|
|
838
896
|
StreamErrorCode.DIAL_BACK_UNREACHABLE.value,
|
|
839
897
|
f"dial-back channel build failed: {exc}",
|
|
840
898
|
)
|
|
841
|
-
return
|
|
899
|
+
return False
|
|
842
900
|
except OSError as exc:
|
|
843
901
|
logger.exception("dial_consumer: channel build failed addr=%s", address, extra=log_extra)
|
|
844
902
|
await _fail(
|
|
845
903
|
StreamErrorCode.DIAL_BACK_UNREACHABLE.value,
|
|
846
904
|
f"dial-back channel build failed: {type(exc).__name__}: {exc}",
|
|
847
905
|
)
|
|
848
|
-
return
|
|
906
|
+
return False
|
|
849
907
|
t_stub = time.perf_counter_ns()
|
|
850
908
|
logger.info("→ Dial-back channel ready to %s", address, extra=log_extra)
|
|
851
909
|
|
|
@@ -878,7 +936,7 @@ class GatewayServicer:
|
|
|
878
936
|
extra=log_extra,
|
|
879
937
|
)
|
|
880
938
|
await release()
|
|
881
|
-
return
|
|
939
|
+
return False
|
|
882
940
|
|
|
883
941
|
# Outbound is StreamServer, inbound is StreamClient — both share
|
|
884
942
|
# field tags so re-wrapping ``_consume_from_redis`` output is a rename.
|
|
@@ -893,8 +951,11 @@ class GatewayServicer:
|
|
|
893
951
|
outgoing_done = asyncio.Event()
|
|
894
952
|
# Consumer's resume cursor, captured from the first reply's ``from_seq``.
|
|
895
953
|
resume_cursor = 0
|
|
954
|
+
# Set once the reader reaches EOS and stream.end is delivered to the consumer.
|
|
955
|
+
delivered_eos = False
|
|
896
956
|
|
|
897
957
|
async def _outgoing() -> AsyncGenerator:
|
|
958
|
+
nonlocal delivered_eos
|
|
898
959
|
try:
|
|
899
960
|
yield init_server
|
|
900
961
|
logger.info(
|
|
@@ -915,6 +976,7 @@ class GatewayServicer:
|
|
|
915
976
|
try:
|
|
916
977
|
cli_msg = await asyncio.wait_for(anext(reader_iter), timeout=idle_timeout)
|
|
917
978
|
except StopAsyncIteration:
|
|
979
|
+
delivered_eos = True
|
|
918
980
|
break
|
|
919
981
|
except asyncio.TimeoutError:
|
|
920
982
|
logger.warning(
|
|
@@ -957,6 +1019,7 @@ class GatewayServicer:
|
|
|
957
1019
|
async def _runner_fatal(code: str, message: str) -> None:
|
|
958
1020
|
await self._emit_fatal_to_redis(task_id, code=code, message=message, log_extra=log_extra)
|
|
959
1021
|
|
|
1022
|
+
retriable = False
|
|
960
1023
|
t_pre_stream = time.perf_counter_ns()
|
|
961
1024
|
logger.info(
|
|
962
1025
|
"[dial-debug] pre_stream dt_since_ready=%.3fms ch_state=%s",
|
|
@@ -1030,7 +1093,7 @@ class GatewayServicer:
|
|
|
1030
1093
|
"gateway has no ModuleRunner configured",
|
|
1031
1094
|
)
|
|
1032
1095
|
output_started.set()
|
|
1033
|
-
return
|
|
1096
|
+
return False
|
|
1034
1097
|
self._spawn(
|
|
1035
1098
|
self._module_runner.run(
|
|
1036
1099
|
upstream.data,
|
|
@@ -1041,6 +1104,7 @@ class GatewayServicer:
|
|
|
1041
1104
|
),
|
|
1042
1105
|
name=f"module_runner_{task_id}",
|
|
1043
1106
|
)
|
|
1107
|
+
on_runner_spawn()
|
|
1044
1108
|
output_started.set()
|
|
1045
1109
|
first = False
|
|
1046
1110
|
continue
|
|
@@ -1082,6 +1146,8 @@ class GatewayServicer:
|
|
|
1082
1146
|
)
|
|
1083
1147
|
# Suppress DIAL_BACK_NO_QUERY in finally — RPC error already emitted.
|
|
1084
1148
|
output_started.set()
|
|
1149
|
+
# Client BiDi died before the stream finished → re-dial candidate.
|
|
1150
|
+
retriable = not delivered_eos
|
|
1085
1151
|
except _GrpcUsageError:
|
|
1086
1152
|
t_fail = time.perf_counter_ns()
|
|
1087
1153
|
logger.warning(
|
|
@@ -1098,6 +1164,7 @@ class GatewayServicer:
|
|
|
1098
1164
|
"dial-back channel closed before BiDi could start",
|
|
1099
1165
|
)
|
|
1100
1166
|
output_started.set()
|
|
1167
|
+
retriable = not delivered_eos
|
|
1101
1168
|
except (RuntimeError, AssertionError, ValueError):
|
|
1102
1169
|
logger.exception("dial_consumer unexpected error", extra=log_extra)
|
|
1103
1170
|
if not output_started.is_set():
|
|
@@ -1120,10 +1187,4 @@ class GatewayServicer:
|
|
|
1120
1187
|
# Unblock _outgoing if consumer never replied.
|
|
1121
1188
|
output_started.set()
|
|
1122
1189
|
await release()
|
|
1123
|
-
|
|
1124
|
-
try:
|
|
1125
|
-
removed = await self._registry.unregister(task_id)
|
|
1126
|
-
if removed is not None:
|
|
1127
|
-
await removed.teardown()
|
|
1128
|
-
except Exception:
|
|
1129
|
-
logger.exception("end-of-stream unregister failed", extra=log_extra)
|
|
1190
|
+
return retriable
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Client-side permission middleware for gRPC service-data access.
|
|
2
|
+
|
|
3
|
+
A single cross-cutting interceptor: any unary call a backend rejects with
|
|
4
|
+
``PERMISSION_DENIED`` surfaces as :class:`PermissionDeniedError`, so callers
|
|
5
|
+
never handle permission per service. Attached on every channel, it covers all
|
|
6
|
+
data services and module-to-module calls uniformly.
|
|
7
|
+
|
|
8
|
+
The interceptor *returns* a terminal denied call rather than raising: raising a
|
|
9
|
+
non-``AioRpcError`` from an aio interceptor leaks into the intercepted call's
|
|
10
|
+
``__del__`` (grpc only swallows ``AioRpcError``/``CancelledError`` there), so we
|
|
11
|
+
mirror grpc's own ``UnaryUnaryCallResponse`` pattern and raise from ``__await__``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import grpc
|
|
19
|
+
import grpc.aio
|
|
20
|
+
|
|
21
|
+
from digitalkin.grpc_servers.exceptions import PermissionDeniedError
|
|
22
|
+
from digitalkin.logger import logger
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Awaitable, Callable
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _DeniedUnaryUnaryCall(grpc.aio.UnaryUnaryCall):
|
|
29
|
+
"""A finished unary call that raises ``PermissionDeniedError`` when awaited."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, method: str, details: str) -> None:
|
|
32
|
+
self._message = f"[{method}] {details or 'permission denied'}"
|
|
33
|
+
self._details = details
|
|
34
|
+
|
|
35
|
+
def cancel(self) -> bool: # noqa: PLR6301
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def cancelled(self) -> bool: # noqa: PLR6301
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def done(self) -> bool: # noqa: PLR6301
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
def add_done_callback(self, unused_callback: Any) -> None:
|
|
45
|
+
"""No-op: the call is already finished.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
unused_callback: Ignored; present to satisfy the call interface.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def time_remaining(self) -> float | None: # noqa: PLR6301
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
async def initial_metadata(self) -> grpc.aio.Metadata: # noqa: PLR6301
|
|
55
|
+
return grpc.aio.Metadata()
|
|
56
|
+
|
|
57
|
+
async def trailing_metadata(self) -> grpc.aio.Metadata: # noqa: PLR6301
|
|
58
|
+
return grpc.aio.Metadata()
|
|
59
|
+
|
|
60
|
+
async def code(self) -> grpc.StatusCode: # noqa: PLR6301
|
|
61
|
+
return grpc.StatusCode.PERMISSION_DENIED
|
|
62
|
+
|
|
63
|
+
async def details(self) -> str:
|
|
64
|
+
return self._details
|
|
65
|
+
|
|
66
|
+
async def debug_error_string(self) -> None: # noqa: PLR6301
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
async def wait_for_connection(self) -> None:
|
|
70
|
+
"""No-op: the call already resolved to a permission denial."""
|
|
71
|
+
|
|
72
|
+
def __await__(self) -> Any:
|
|
73
|
+
raise PermissionDeniedError(self._message)
|
|
74
|
+
yield # unreachable; marks this method a generator so it is awaitable
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PermissionClientInterceptor(grpc.aio.UnaryUnaryClientInterceptor):
|
|
78
|
+
"""Map a ``PERMISSION_DENIED`` unary reply to ``PermissionDeniedError``."""
|
|
79
|
+
|
|
80
|
+
async def intercept_unary_unary( # noqa: PLR6301
|
|
81
|
+
self,
|
|
82
|
+
continuation: Callable[[grpc.aio.ClientCallDetails, Any], Awaitable[Any]],
|
|
83
|
+
client_call_details: grpc.aio.ClientCallDetails,
|
|
84
|
+
request: Any,
|
|
85
|
+
) -> Any:
|
|
86
|
+
"""Return a denied call on PERMISSION_DENIED, else pass the real call through.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
continuation: Downstream call continuation.
|
|
90
|
+
client_call_details: Original call details.
|
|
91
|
+
request: Request message.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The downstream call, or a terminal call raising PermissionDeniedError on await.
|
|
95
|
+
"""
|
|
96
|
+
call = await continuation(client_call_details, request)
|
|
97
|
+
if await call.code() == grpc.StatusCode.PERMISSION_DENIED:
|
|
98
|
+
method = client_call_details.method
|
|
99
|
+
method_name = method.decode() if isinstance(method, bytes) else method
|
|
100
|
+
details = await call.details()
|
|
101
|
+
logger.warning("permission denied on %s: %s", method_name, details or "")
|
|
102
|
+
return _DeniedUnaryUnaryCall(method_name, details)
|
|
103
|
+
return call
|
|
@@ -16,6 +16,7 @@ import grpc.aio
|
|
|
16
16
|
|
|
17
17
|
from digitalkin.core.resilience.bulkhead import Bulkhead
|
|
18
18
|
from digitalkin.grpc_servers.exceptions import CircuitOpenError, ServerError
|
|
19
|
+
from digitalkin.grpc_servers.interceptors.permission import PermissionClientInterceptor
|
|
19
20
|
from digitalkin.grpc_servers.interceptors.request_ids import RequestIdClientInterceptor
|
|
20
21
|
from digitalkin.grpc_servers.utils.circuit_breaker import CircuitBreaker
|
|
21
22
|
from digitalkin.logger import logger
|
|
@@ -103,7 +104,7 @@ class GrpcClientWrapper:
|
|
|
103
104
|
|
|
104
105
|
credentials = self._build_channel_credentials(config)
|
|
105
106
|
grpc_compression = config.compression.to_grpc()
|
|
106
|
-
interceptors = [RequestIdClientInterceptor()]
|
|
107
|
+
interceptors = [RequestIdClientInterceptor(), PermissionClientInterceptor()]
|
|
107
108
|
if credentials is not None:
|
|
108
109
|
channel = grpc.aio.secure_channel(
|
|
109
110
|
config.address,
|
|
@@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator
|
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from digitalkin.grpc_servers.exceptions import ServerError
|
|
7
|
+
from digitalkin.grpc_servers.exceptions import PermissionDeniedError, ServerError
|
|
8
8
|
from digitalkin.logger import logger
|
|
9
9
|
|
|
10
10
|
|
|
@@ -28,6 +28,7 @@ class GrpcErrorHandlerMixin:
|
|
|
28
28
|
Context for the operation.
|
|
29
29
|
|
|
30
30
|
Raises:
|
|
31
|
+
PermissionDeniedError: Re-raised as-is so the authz status is never masked.
|
|
31
32
|
ServerError: For gRPC-related errors.
|
|
32
33
|
service_error_class: For service-specific errors if provided.
|
|
33
34
|
"""
|
|
@@ -36,6 +37,8 @@ class GrpcErrorHandlerMixin:
|
|
|
36
37
|
|
|
37
38
|
try:
|
|
38
39
|
yield
|
|
40
|
+
except PermissionDeniedError:
|
|
41
|
+
raise
|
|
39
42
|
except service_error_class as e:
|
|
40
43
|
# Re-raise service-specific errors as-is
|
|
41
44
|
msg = f"{service_error_class.__name__} in {operation}: {e}"
|
|
@@ -11,7 +11,10 @@ class GatewayStreamSettings(BaseSettings):
|
|
|
11
11
|
|
|
12
12
|
model_config = SettingsConfigDict(env_prefix="DIGITALKIN_GATEWAY_STREAM_", case_sensitive=False)
|
|
13
13
|
|
|
14
|
-
redis_stream_ttl: int = Field(
|
|
14
|
+
redis_stream_ttl: int = Field(
|
|
15
|
+
default=360,
|
|
16
|
+
description="Stream TTL (s) after EOS; >= reconnect window so a completed stream survives a reboot",
|
|
17
|
+
)
|
|
15
18
|
redis_stream_initial_ttl: int = Field(default=600, description="Stream TTL in seconds before EOS")
|
|
16
19
|
redis_stream_maxlen: int = Field(default=1000, gt=0, description="Approximate max entries before trimming")
|
|
17
20
|
redis_cursor_ttl: int = Field(default=360, description="Cursor key TTL in seconds")
|
|
@@ -113,6 +116,23 @@ class GatewayQueueSettings(BaseSettings):
|
|
|
113
116
|
)
|
|
114
117
|
|
|
115
118
|
|
|
119
|
+
class GatewayDialReconnectSettings(BaseSettings):
|
|
120
|
+
"""Server-side dial-back auto-reconnect window and backoff."""
|
|
121
|
+
|
|
122
|
+
model_config = SettingsConfigDict(env_prefix="DIGITALKIN_GATEWAY_DIAL_BACK_RECONNECT_", case_sensitive=False)
|
|
123
|
+
|
|
124
|
+
window_s: float = Field(
|
|
125
|
+
default=300.0,
|
|
126
|
+
description=(
|
|
127
|
+
"Total time, measured from the first detected disconnect, that the gateway keeps "
|
|
128
|
+
"re-dialing a dead consumer so a client that lost internet or rebooted can re-attach "
|
|
129
|
+
"and resume from its cursor. Keep <= the post-EOS stream TTL."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
backoff_base_s: float = Field(default=3.0, description="Base delay between re-dial attempts (full-jittered).")
|
|
133
|
+
backoff_max_s: float = Field(default=5.0, description="Cap on the re-dial backoff delay.")
|
|
134
|
+
|
|
135
|
+
|
|
116
136
|
class GatewaySettings(BaseSettings):
|
|
117
137
|
"""Top-level gateway configuration."""
|
|
118
138
|
|
|
@@ -149,6 +169,7 @@ class GatewaySettings(BaseSettings):
|
|
|
149
169
|
stream: GatewayStreamSettings = Field(default_factory=GatewayStreamSettings)
|
|
150
170
|
queue: GatewayQueueSettings = Field(default_factory=GatewayQueueSettings)
|
|
151
171
|
m2m: GatewayM2MSettings = Field(default_factory=GatewayM2MSettings)
|
|
172
|
+
dial_reconnect: GatewayDialReconnectSettings = Field(default_factory=GatewayDialReconnectSettings)
|
|
152
173
|
|
|
153
174
|
|
|
154
175
|
@lru_cache(maxsize=1)
|
|
@@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
|
|
|
7
7
|
from collections.abc import Callable, Coroutine
|
|
8
8
|
from typing import Any, ClassVar, Generic
|
|
9
9
|
|
|
10
|
+
from digitalkin.grpc_servers.exceptions import PermissionDeniedError
|
|
10
11
|
from digitalkin.grpc_servers.utils.utility_schema_extender import UtilitySchemaExtender
|
|
11
12
|
from digitalkin.logger import logger
|
|
12
13
|
from digitalkin.models.module.module import ModuleCodeModel, ModuleStatus
|
|
@@ -548,6 +549,10 @@ class BaseModule( # Module SDK base class requires many public methods # noqa:
|
|
|
548
549
|
self._status = ModuleStatus.CANCELLED
|
|
549
550
|
logger.info("Module %s cancelled", self.name, extra=self.context.session.current_ids())
|
|
550
551
|
raise
|
|
552
|
+
except PermissionDeniedError as e:
|
|
553
|
+
self._status = ModuleStatus.FAILED
|
|
554
|
+
logger.warning("Permission denied in module %s: %s", self.name, e, extra=self.context.session.current_ids())
|
|
555
|
+
await self._notify_permission_denied(self.context.callbacks.send_message, e)
|
|
551
556
|
except Exception as e:
|
|
552
557
|
self._status = ModuleStatus.FAILED
|
|
553
558
|
logger.exception("Error inside module %s", self.name, extra=self.context.session.current_ids())
|
|
@@ -564,6 +569,28 @@ class BaseModule( # Module SDK base class requires many public methods # noqa:
|
|
|
564
569
|
else:
|
|
565
570
|
self._status = ModuleStatus.STOPPING
|
|
566
571
|
|
|
572
|
+
async def _notify_permission_denied(
|
|
573
|
+
self,
|
|
574
|
+
callback: Callable[..., Coroutine[Any, Any, None]],
|
|
575
|
+
e: PermissionDeniedError,
|
|
576
|
+
) -> None:
|
|
577
|
+
"""Send a PermissionDenied notification to the user via the callback.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
callback: The output callback installed on the module context.
|
|
581
|
+
e: The permission-denied error raised by a service call.
|
|
582
|
+
"""
|
|
583
|
+
try:
|
|
584
|
+
await callback(
|
|
585
|
+
ModuleCodeModel(
|
|
586
|
+
code="PermissionDenied",
|
|
587
|
+
short_description="Permission denied",
|
|
588
|
+
message=str(e),
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
except Exception:
|
|
592
|
+
logger.exception("Failed to send permission-denied callback", extra=self.context.session.current_ids())
|
|
593
|
+
|
|
567
594
|
async def prepare(
|
|
568
595
|
self,
|
|
569
596
|
setup_data: SetupModelT,
|
|
@@ -623,6 +650,14 @@ class BaseModule( # Module SDK base class requires many public methods # noqa:
|
|
|
623
650
|
try:
|
|
624
651
|
await self.prepare(setup_data, callback)
|
|
625
652
|
timer.mark("prepare")
|
|
653
|
+
except PermissionDeniedError as e:
|
|
654
|
+
self._status = ModuleStatus.FAILED
|
|
655
|
+
logger.warning("Permission denied initializing module: %s", e, extra=self.context.session.current_ids())
|
|
656
|
+
await self._notify_permission_denied(callback, e)
|
|
657
|
+
if done_callback is not None:
|
|
658
|
+
await done_callback(None)
|
|
659
|
+
await self.stop()
|
|
660
|
+
return
|
|
626
661
|
except Exception as e:
|
|
627
662
|
self._status = ModuleStatus.FAILED
|
|
628
663
|
short_description = "Error initializing module"
|