digitalkin 0.3.1.dev2__py3-none-any.whl → 0.3.2a3__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.
Files changed (87) hide show
  1. base_server/server_async_insecure.py +6 -5
  2. base_server/server_async_secure.py +6 -5
  3. base_server/server_sync_insecure.py +5 -4
  4. base_server/server_sync_secure.py +5 -4
  5. digitalkin/__version__.py +1 -1
  6. digitalkin/core/job_manager/base_job_manager.py +1 -1
  7. digitalkin/core/job_manager/single_job_manager.py +78 -36
  8. digitalkin/core/job_manager/taskiq_broker.py +7 -6
  9. digitalkin/core/job_manager/taskiq_job_manager.py +9 -5
  10. digitalkin/core/task_manager/base_task_manager.py +3 -1
  11. digitalkin/core/task_manager/surrealdb_repository.py +29 -7
  12. digitalkin/core/task_manager/task_executor.py +46 -12
  13. digitalkin/core/task_manager/task_session.py +132 -102
  14. digitalkin/grpc_servers/module_server.py +95 -171
  15. digitalkin/grpc_servers/module_servicer.py +121 -19
  16. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +36 -10
  17. digitalkin/grpc_servers/utils/utility_schema_extender.py +106 -0
  18. digitalkin/models/__init__.py +1 -1
  19. digitalkin/models/core/job_manager_models.py +0 -8
  20. digitalkin/models/core/task_monitor.py +23 -1
  21. digitalkin/models/grpc_servers/models.py +95 -8
  22. digitalkin/models/module/__init__.py +26 -13
  23. digitalkin/models/module/base_types.py +61 -0
  24. digitalkin/models/module/module_context.py +279 -13
  25. digitalkin/models/module/module_types.py +28 -392
  26. digitalkin/models/module/setup_types.py +547 -0
  27. digitalkin/models/module/tool_cache.py +230 -0
  28. digitalkin/models/module/tool_reference.py +160 -0
  29. digitalkin/models/module/utility.py +167 -0
  30. digitalkin/models/services/cost.py +22 -1
  31. digitalkin/models/services/registry.py +77 -0
  32. digitalkin/modules/__init__.py +5 -1
  33. digitalkin/modules/_base_module.py +188 -63
  34. digitalkin/modules/archetype_module.py +6 -1
  35. digitalkin/modules/tool_module.py +6 -1
  36. digitalkin/modules/triggers/__init__.py +8 -0
  37. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  38. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  39. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  40. digitalkin/services/__init__.py +4 -0
  41. digitalkin/services/communication/__init__.py +7 -0
  42. digitalkin/services/communication/communication_strategy.py +87 -0
  43. digitalkin/services/communication/default_communication.py +104 -0
  44. digitalkin/services/communication/grpc_communication.py +264 -0
  45. digitalkin/services/cost/cost_strategy.py +36 -14
  46. digitalkin/services/cost/default_cost.py +61 -1
  47. digitalkin/services/cost/grpc_cost.py +98 -2
  48. digitalkin/services/filesystem/grpc_filesystem.py +9 -2
  49. digitalkin/services/registry/__init__.py +22 -1
  50. digitalkin/services/registry/default_registry.py +156 -4
  51. digitalkin/services/registry/exceptions.py +47 -0
  52. digitalkin/services/registry/grpc_registry.py +382 -0
  53. digitalkin/services/registry/registry_models.py +15 -0
  54. digitalkin/services/registry/registry_strategy.py +106 -4
  55. digitalkin/services/services_config.py +25 -3
  56. digitalkin/services/services_models.py +5 -1
  57. digitalkin/services/setup/default_setup.py +1 -1
  58. digitalkin/services/setup/grpc_setup.py +1 -1
  59. digitalkin/services/storage/grpc_storage.py +1 -1
  60. digitalkin/services/user_profile/__init__.py +11 -0
  61. digitalkin/services/user_profile/grpc_user_profile.py +2 -2
  62. digitalkin/services/user_profile/user_profile_strategy.py +0 -15
  63. digitalkin/utils/__init__.py +15 -3
  64. digitalkin/utils/conditional_schema.py +260 -0
  65. digitalkin/utils/dynamic_schema.py +4 -0
  66. digitalkin/utils/schema_splitter.py +290 -0
  67. {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/METADATA +12 -12
  68. digitalkin-0.3.2a3.dist-info/RECORD +144 -0
  69. {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/WHEEL +1 -1
  70. {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/top_level.txt +1 -0
  71. modules/archetype_with_tools_module.py +232 -0
  72. modules/cpu_intensive_module.py +1 -1
  73. modules/dynamic_setup_module.py +5 -29
  74. modules/minimal_llm_module.py +1 -1
  75. modules/text_transform_module.py +1 -1
  76. monitoring/digitalkin_observability/__init__.py +46 -0
  77. monitoring/digitalkin_observability/http_server.py +150 -0
  78. monitoring/digitalkin_observability/interceptors.py +176 -0
  79. monitoring/digitalkin_observability/metrics.py +201 -0
  80. monitoring/digitalkin_observability/prometheus.py +137 -0
  81. monitoring/tests/test_metrics.py +172 -0
  82. services/filesystem_module.py +7 -5
  83. services/storage_module.py +4 -2
  84. digitalkin/grpc_servers/registry_server.py +0 -65
  85. digitalkin/grpc_servers/registry_servicer.py +0 -456
  86. digitalkin-0.3.1.dev2.dist-info/RECORD +0 -119
  87. {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,172 @@
1
+ """Tests for metrics collection.
2
+
3
+ Run with: python -m pytest tests/test_metrics.py
4
+ """
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ # Add the parent directory to the path so we can import digitalkin_observability
12
+ sys.path.insert(0, str(Path(__file__).parent.parent))
13
+
14
+ from digitalkin_observability import MetricsCollector, PrometheusExporter, get_metrics
15
+
16
+
17
+ class TestMetricsCollector:
18
+ """Tests for MetricsCollector singleton."""
19
+
20
+ def setup_method(self) -> None:
21
+ """Reset metrics before each test."""
22
+ get_metrics().reset()
23
+
24
+ def test_singleton_returns_same_instance(self) -> None:
25
+ """Test that get_metrics returns the same instance."""
26
+ m1 = get_metrics()
27
+ m2 = get_metrics()
28
+ assert m1 is m2
29
+
30
+ def test_inc_jobs_started(self) -> None:
31
+ """Test incrementing jobs started counter."""
32
+ metrics = get_metrics()
33
+ metrics.inc_jobs_started("TestModule")
34
+
35
+ assert metrics.jobs_started_total == 1
36
+ assert metrics.active_jobs == 1
37
+
38
+ def test_inc_jobs_completed(self) -> None:
39
+ """Test incrementing jobs completed counter."""
40
+ metrics = get_metrics()
41
+ metrics.inc_jobs_started("TestModule")
42
+ metrics.inc_jobs_completed("TestModule", 1.5)
43
+
44
+ assert metrics.jobs_completed_total == 1
45
+ assert metrics.active_jobs == 0
46
+ assert metrics.job_duration_seconds.count == 1
47
+ assert metrics.job_duration_seconds.total_sum == 1.5
48
+
49
+ def test_inc_jobs_failed(self) -> None:
50
+ """Test incrementing jobs failed counter."""
51
+ metrics = get_metrics()
52
+ metrics.inc_jobs_started("TestModule")
53
+ metrics.inc_jobs_failed("TestModule")
54
+
55
+ assert metrics.jobs_failed_total == 1
56
+ assert metrics.active_jobs == 0
57
+
58
+ def test_inc_jobs_cancelled(self) -> None:
59
+ """Test incrementing jobs cancelled counter."""
60
+ metrics = get_metrics()
61
+ metrics.inc_jobs_started("TestModule")
62
+ metrics.inc_jobs_cancelled("TestModule")
63
+
64
+ assert metrics.jobs_cancelled_total == 1
65
+ assert metrics.active_jobs == 0
66
+
67
+ def test_inc_messages_sent(self) -> None:
68
+ """Test incrementing messages sent counter."""
69
+ metrics = get_metrics()
70
+ metrics.inc_messages_sent("message")
71
+ metrics.inc_messages_sent("file")
72
+ metrics.inc_messages_sent()
73
+
74
+ assert metrics.messages_sent_total == 3
75
+
76
+ def test_queue_depth_tracking(self) -> None:
77
+ """Test queue depth tracking."""
78
+ metrics = get_metrics()
79
+ metrics.set_queue_depth("job1", 5)
80
+ metrics.set_queue_depth("job2", 3)
81
+
82
+ assert metrics.queue_depth["job1"] == 5
83
+ assert metrics.queue_depth["job2"] == 3
84
+
85
+ metrics.clear_queue_depth("job1")
86
+ assert "job1" not in metrics.queue_depth
87
+
88
+ def test_snapshot(self) -> None:
89
+ """Test snapshot returns all metrics."""
90
+ metrics = get_metrics()
91
+ metrics.inc_jobs_started("TestModule")
92
+ metrics.inc_jobs_completed("TestModule", 0.5)
93
+ metrics.inc_messages_sent("message")
94
+
95
+ snapshot = metrics.snapshot()
96
+
97
+ assert snapshot["jobs_started_total"] == 1
98
+ assert snapshot["jobs_completed_total"] == 1
99
+ assert snapshot["messages_sent_total"] == 1
100
+ assert "job_duration_seconds" in snapshot
101
+ assert "by_module" in snapshot
102
+ assert "TestModule" in snapshot["by_module"]
103
+
104
+ def test_histogram_observe(self) -> None:
105
+ """Test histogram observations."""
106
+ metrics = get_metrics()
107
+ metrics.observe_grpc_duration(0.05)
108
+ metrics.observe_grpc_duration(0.15)
109
+
110
+ assert metrics.grpc_request_duration_seconds.count == 2
111
+ assert metrics.grpc_request_duration_seconds.total_sum == pytest.approx(0.2)
112
+
113
+ def test_reset_clears_all_metrics(self) -> None:
114
+ """Test reset clears all metrics."""
115
+ metrics = get_metrics()
116
+ metrics.inc_jobs_started("TestModule")
117
+ metrics.inc_errors()
118
+
119
+ metrics.reset()
120
+
121
+ assert metrics.jobs_started_total == 0
122
+ assert metrics.errors_total == 0
123
+ assert metrics.active_jobs == 0
124
+
125
+
126
+ class TestPrometheusExporter:
127
+ """Tests for Prometheus exporter."""
128
+
129
+ def setup_method(self) -> None:
130
+ """Reset metrics before each test."""
131
+ get_metrics().reset()
132
+
133
+ def test_export_returns_string(self) -> None:
134
+ """Test that export returns a string."""
135
+ output = PrometheusExporter.export()
136
+ assert isinstance(output, str)
137
+
138
+ def test_export_contains_job_counters(self) -> None:
139
+ """Test export contains job counters."""
140
+ metrics = get_metrics()
141
+ metrics.inc_jobs_started("TestModule")
142
+
143
+ output = PrometheusExporter.export()
144
+
145
+ assert "digitalkin_jobs_started_total 1" in output
146
+ assert "digitalkin_active_jobs 1" in output
147
+
148
+ def test_export_contains_histogram(self) -> None:
149
+ """Test export contains histogram data."""
150
+ metrics = get_metrics()
151
+ metrics.observe_grpc_duration(0.05)
152
+
153
+ output = PrometheusExporter.export()
154
+
155
+ assert "digitalkin_grpc_request_duration_seconds" in output
156
+ assert "# TYPE digitalkin_grpc_request_duration_seconds histogram" in output
157
+
158
+ def test_export_contains_module_breakdown(self) -> None:
159
+ """Test export contains per-module breakdown."""
160
+ metrics = get_metrics()
161
+ metrics.inc_jobs_started("MyModule")
162
+
163
+ output = PrometheusExporter.export()
164
+
165
+ assert 'digitalkin_jobs_by_module{module="MyModule",status="started"} 1' in output
166
+
167
+ def test_export_contains_help_and_type(self) -> None:
168
+ """Test export contains HELP and TYPE comments."""
169
+ output = PrometheusExporter.export()
170
+
171
+ assert "# HELP digitalkin_jobs_started_total" in output
172
+ assert "# TYPE digitalkin_jobs_started_total counter" in output
@@ -3,16 +3,16 @@
3
3
  import asyncio
4
4
  import datetime
5
5
  from collections.abc import Callable
6
- from typing import TYPE_CHECKING, Any
6
+ from typing import Any
7
7
 
8
8
  from pydantic import BaseModel, Field
9
9
 
10
10
  from digitalkin.logger import logger
11
11
  from digitalkin.models.module import ModuleStatus
12
12
  from digitalkin.modules.archetype_module import ArchetypeModule
13
+ from digitalkin.services.filesystem.filesystem_strategy import FileFilter, UploadFileData
13
14
  from digitalkin.services.services_config import ServicesConfig
14
15
  from digitalkin.services.services_models import ServicesMode
15
- from digitalkin.services.filesystem.filesystem_strategy import FilesystemRecord, FileFilter, UploadFileData
16
16
 
17
17
 
18
18
  class ExampleInput(BaseModel):
@@ -62,7 +62,10 @@ class ExampleModule(ArchetypeModule[ExampleInput, ExampleOutput, ExampleSetup, E
62
62
 
63
63
  # Define services_config_params with default values
64
64
  services_config_strategies = {}
65
- services_config_params = {"cost": {"config": {}}, "storage": {"config": {}}} # Filesystem has no config but it's enabled
65
+ services_config_params = {
66
+ "cost": {"config": {}},
67
+ "storage": {"config": {}},
68
+ } # Filesystem has no config but it's enabled
66
69
 
67
70
  def __init__(self, job_id: str, mission_id: str, setup_version_id: str) -> None:
68
71
  """Initialize the example module.
@@ -128,7 +131,6 @@ class ExampleModule(ArchetypeModule[ExampleInput, ExampleOutput, ExampleSetup, E
128
131
  callback(record.model_dump())
129
132
  # Call the callback with the output data
130
133
 
131
-
132
134
  # Wait a bit to simulate processing time
133
135
  await asyncio.sleep(1)
134
136
 
@@ -173,7 +175,7 @@ async def test_module() -> None:
173
175
 
174
176
  # Check the storage
175
177
  if module.status == ModuleStatus.STOPPED:
176
- files, nb_results = module.filesystem.get_files(
178
+ files, _nb_results = module.filesystem.get_files(
177
179
  filters=FileFilter(name="example_output.txt", context="test-mission-123"),
178
180
  )
179
181
  for file in files:
@@ -64,7 +64,7 @@ class ExampleModule(ArchetypeModule[ExampleInput, ExampleOutput, ExampleSetup, E
64
64
 
65
65
  # Define services_config_params with default values
66
66
  services_config_strategies = {}
67
- services_config_params = {"storage": {"config": {"example": ExampleOutput}},"cost": {"config":{}}}
67
+ services_config_params = {"storage": {"config": {"example": ExampleOutput}}, "cost": {"config": {}}}
68
68
 
69
69
  def __init__(self, job_id: str, mission_id: str, setup_version_id: str) -> None:
70
70
  """Initialize the example module.
@@ -184,7 +184,9 @@ async def test_module() -> None:
184
184
  def test_storage_directly() -> None:
185
185
  """Test the storage service directly."""
186
186
  # Initialize storage service
187
- storage = ServicesConfig().storage(mission_id="test-mission",setup_version_id="test-setup-123", config={"example": ExampleStorage})
187
+ storage = ServicesConfig().storage(
188
+ mission_id="test-mission", setup_version_id="test-setup-123", config={"example": ExampleStorage}
189
+ )
188
190
 
189
191
  # Create a test record
190
192
  storage.store("example", "test_table", {"test_key": "test_value"}, "OUTPUT")
@@ -1,65 +0,0 @@
1
- """Registry gRPC server implementation for DigitalKin."""
2
-
3
- from digitalkin_proto.agentic_mesh_protocol.module_registry.v1 import (
4
- module_registry_service_pb2,
5
- module_registry_service_pb2_grpc,
6
- )
7
-
8
- from digitalkin.grpc_servers._base_server import BaseServer
9
- from digitalkin.grpc_servers.registry_servicer import RegistryModule, RegistryServicer
10
- from digitalkin.logger import logger
11
- from digitalkin.models.grpc_servers.models import RegistryServerConfig
12
-
13
-
14
- class RegistryServer(BaseServer):
15
- """gRPC server for DigitalKin module registry.
16
-
17
- This server implements the ModuleRegistryService which allows modules to register
18
- themselves and be discovered by other components in the system.
19
-
20
- Attributes:
21
- config: Server configuration.
22
- registry_servicer: The gRPC servicer handling registry requests.
23
- """
24
-
25
- def __init__(
26
- self,
27
- config: RegistryServerConfig,
28
- ) -> None:
29
- """Initialize the registry server.
30
-
31
- Args:
32
- config: Server configuration.
33
- """
34
- super().__init__(config)
35
- self.config = config
36
- self.registry_servicer: RegistryServicer | None = None
37
-
38
- def _register_servicers(self) -> None:
39
- """Register the registry servicer with the gRPC server.
40
-
41
- Raises:
42
- RuntimeError: If server is not registered during server creation
43
- """
44
- if self.server is None:
45
- msg = "Server must be created before registering servicers"
46
- raise RuntimeError(msg)
47
-
48
- logger.debug("Registering registry servicer")
49
- self.registry_servicer = RegistryServicer()
50
- self.register_servicer(
51
- self.registry_servicer,
52
- module_registry_service_pb2_grpc.add_ModuleRegistryServiceServicer_to_server,
53
- service_descriptor=module_registry_service_pb2.DESCRIPTOR,
54
- )
55
- logger.debug("Registered registry servicer")
56
-
57
- def get_registered_modules(self) -> list[RegistryModule]:
58
- """Get a list of all registered modules.
59
-
60
- Returns:
61
- A list of module information objects.
62
- """
63
- if self.registry_servicer:
64
- return list(self.registry_servicer.registered_modules.values())
65
- return []