homesec 0.1.1__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- homesec/app.py +34 -36
- homesec/cli.py +14 -11
- homesec/config/loader.py +11 -11
- homesec/config/validation.py +2 -5
- homesec/errors.py +2 -4
- homesec/health/server.py +29 -27
- homesec/interfaces.py +11 -6
- homesec/logging_setup.py +9 -5
- homesec/maintenance/cleanup_clips.py +2 -3
- homesec/models/__init__.py +1 -1
- homesec/models/alert.py +2 -0
- homesec/models/clip.py +8 -1
- homesec/models/config.py +9 -13
- homesec/models/events.py +14 -0
- homesec/models/filter.py +1 -3
- homesec/models/vlm.py +1 -2
- homesec/pipeline/core.py +15 -32
- homesec/plugins/alert_policies/__init__.py +3 -4
- homesec/plugins/alert_policies/default.py +3 -2
- homesec/plugins/alert_policies/noop.py +1 -2
- homesec/plugins/analyzers/__init__.py +3 -4
- homesec/plugins/analyzers/openai.py +34 -43
- homesec/plugins/filters/__init__.py +3 -4
- homesec/plugins/filters/yolo.py +27 -29
- homesec/plugins/notifiers/__init__.py +2 -1
- homesec/plugins/notifiers/mqtt.py +16 -17
- homesec/plugins/notifiers/multiplex.py +3 -2
- homesec/plugins/notifiers/sendgrid_email.py +6 -8
- homesec/plugins/storage/__init__.py +3 -4
- homesec/plugins/storage/dropbox.py +20 -17
- homesec/plugins/storage/local.py +3 -1
- homesec/plugins/utils.py +2 -1
- homesec/repository/clip_repository.py +5 -4
- homesec/sources/base.py +2 -2
- homesec/sources/local_folder.py +9 -7
- homesec/sources/rtsp.py +22 -10
- homesec/state/postgres.py +34 -35
- homesec/telemetry/db_log_handler.py +3 -2
- {homesec-0.1.1.dist-info → homesec-1.0.1.dist-info}/METADATA +39 -31
- homesec-1.0.1.dist-info/RECORD +62 -0
- homesec-0.1.1.dist-info/RECORD +0 -62
- {homesec-0.1.1.dist-info → homesec-1.0.1.dist-info}/WHEEL +0 -0
- {homesec-0.1.1.dist-info → homesec-1.0.1.dist-info}/entry_points.txt +0 -0
- {homesec-0.1.1.dist-info → homesec-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -62,7 +62,7 @@ class _Counts:
|
|
|
62
62
|
delete_errors: int = 0
|
|
63
63
|
state_errors: int = 0
|
|
64
64
|
|
|
65
|
-
def __add__(self, other:
|
|
65
|
+
def __add__(self, other: _Counts) -> _Counts:
|
|
66
66
|
return _Counts(
|
|
67
67
|
scanned_rows=self.scanned_rows + other.scanned_rows,
|
|
68
68
|
candidates=self.candidates + other.candidates,
|
|
@@ -206,8 +206,7 @@ async def run_cleanup(opts: CleanupOptions) -> None:
|
|
|
206
206
|
candidates: list[tuple[str, ClipStateData, datetime]] = [
|
|
207
207
|
(clip_id, state, created_at)
|
|
208
208
|
for clip_id, state, created_at in rows
|
|
209
|
-
if state.filter_result is not None
|
|
210
|
-
and not state.filter_result.detected_classes
|
|
209
|
+
if state.filter_result is not None and not state.filter_result.detected_classes
|
|
211
210
|
]
|
|
212
211
|
totals = totals + _Counts(candidates=len(candidates))
|
|
213
212
|
|
homesec/models/__init__.py
CHANGED
|
@@ -5,11 +5,11 @@ from homesec.models.clip import Clip, ClipStateData, _resolve_forward_refs
|
|
|
5
5
|
from homesec.models.config import (
|
|
6
6
|
AlertPolicyConfig,
|
|
7
7
|
AlertPolicyOverrides,
|
|
8
|
-
DefaultAlertPolicySettings,
|
|
9
8
|
CameraConfig,
|
|
10
9
|
CameraSourceConfig,
|
|
11
10
|
ConcurrencyConfig,
|
|
12
11
|
Config,
|
|
12
|
+
DefaultAlertPolicySettings,
|
|
13
13
|
DropboxStorageConfig,
|
|
14
14
|
HealthConfig,
|
|
15
15
|
LocalStorageConfig,
|
homesec/models/alert.py
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from datetime import datetime
|
|
6
|
+
|
|
6
7
|
from pydantic import BaseModel
|
|
7
8
|
|
|
8
9
|
from homesec.models.vlm import RiskLevel, SequenceAnalysis
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
class AlertDecision(BaseModel):
|
|
11
13
|
"""Decision whether to send an alert for a clip."""
|
|
12
14
|
|
homesec/models/clip.py
CHANGED
|
@@ -64,8 +64,15 @@ class ClipStateData(BaseModel):
|
|
|
64
64
|
# Resolve forward references after imports are available
|
|
65
65
|
def _resolve_forward_refs() -> None:
|
|
66
66
|
"""Resolve forward references in ClipStateData."""
|
|
67
|
+
# Explicitly import types to make them available for model_rebuild
|
|
67
68
|
from homesec.models.alert import AlertDecision
|
|
68
69
|
from homesec.models.filter import FilterResult
|
|
69
70
|
from homesec.models.vlm import AnalysisResult
|
|
70
71
|
|
|
71
|
-
ClipStateData.model_rebuild(
|
|
72
|
+
ClipStateData.model_rebuild(
|
|
73
|
+
_types_namespace={
|
|
74
|
+
"FilterResult": FilterResult,
|
|
75
|
+
"AnalysisResult": AnalysisResult,
|
|
76
|
+
"AlertDecision": AlertDecision,
|
|
77
|
+
}
|
|
78
|
+
)
|
homesec/models/config.py
CHANGED
|
@@ -9,10 +9,8 @@ from pydantic import BaseModel, Field, model_validator
|
|
|
9
9
|
from homesec.models.filter import FilterConfig
|
|
10
10
|
from homesec.models.source import FtpSourceConfig, LocalFolderSourceConfig, RTSPSourceConfig
|
|
11
11
|
from homesec.models.vlm import (
|
|
12
|
-
OpenAILLMConfig,
|
|
13
12
|
RiskLevel,
|
|
14
13
|
VLMConfig,
|
|
15
|
-
VLMPreprocessConfig,
|
|
16
14
|
)
|
|
17
15
|
|
|
18
16
|
|
|
@@ -40,7 +38,7 @@ class AlertPolicyConfig(BaseModel):
|
|
|
40
38
|
config: dict[str, object] = Field(default_factory=dict)
|
|
41
39
|
|
|
42
40
|
@model_validator(mode="after")
|
|
43
|
-
def _validate_alert_policy(self) ->
|
|
41
|
+
def _validate_alert_policy(self) -> AlertPolicyConfig:
|
|
44
42
|
if self.backend == "default":
|
|
45
43
|
DefaultAlertPolicySettings.model_validate(self.config)
|
|
46
44
|
return self
|
|
@@ -86,7 +84,7 @@ class StorageConfig(BaseModel):
|
|
|
86
84
|
paths: StoragePathsConfig = Field(default_factory=StoragePathsConfig)
|
|
87
85
|
|
|
88
86
|
@model_validator(mode="after")
|
|
89
|
-
def _validate_builtin_backends(self) ->
|
|
87
|
+
def _validate_builtin_backends(self) -> StorageConfig:
|
|
90
88
|
"""Validate that built-in backends have their required config.
|
|
91
89
|
|
|
92
90
|
Third-party backends are validated later in create_storage().
|
|
@@ -117,7 +115,7 @@ class StateStoreConfig(BaseModel):
|
|
|
117
115
|
dsn: str | None = None
|
|
118
116
|
|
|
119
117
|
@model_validator(mode="after")
|
|
120
|
-
def _validate_backend(self) ->
|
|
118
|
+
def _validate_backend(self) -> StateStoreConfig:
|
|
121
119
|
if not (self.dsn_env or self.dsn):
|
|
122
120
|
raise ValueError("state_store.dsn_env or state_store.dsn required for postgres")
|
|
123
121
|
return self
|
|
@@ -174,7 +172,7 @@ class SendGridEmailConfig(BaseModel):
|
|
|
174
172
|
"<p><strong>Activity:</strong> {activity_type}</p>"
|
|
175
173
|
"<p><strong>Reason:</strong> {notify_reason}</p>"
|
|
176
174
|
"<p><strong>Summary:</strong> {summary}</p>"
|
|
177
|
-
|
|
175
|
+
'<p><strong>View:</strong> <a href="{view_url}">{view_url}</a></p>'
|
|
178
176
|
"<p><strong>Storage:</strong> {storage_uri}</p>"
|
|
179
177
|
"<p><strong>Time:</strong> {ts}</p>"
|
|
180
178
|
"<p><strong>Upload failed:</strong> {upload_failed}</p>"
|
|
@@ -186,7 +184,7 @@ class SendGridEmailConfig(BaseModel):
|
|
|
186
184
|
api_base: str = "https://api.sendgrid.com/v3"
|
|
187
185
|
|
|
188
186
|
@model_validator(mode="after")
|
|
189
|
-
def _validate_templates(self) ->
|
|
187
|
+
def _validate_templates(self) -> SendGridEmailConfig:
|
|
190
188
|
if not self.text_template and not self.html_template:
|
|
191
189
|
raise ValueError("sendgrid_email requires at least one of text_template/html_template")
|
|
192
190
|
return self
|
|
@@ -245,9 +243,7 @@ class CameraSourceConfig(BaseModel):
|
|
|
245
243
|
return data
|
|
246
244
|
source_type = data.get("type")
|
|
247
245
|
raw_config = data.get("config")
|
|
248
|
-
if isinstance(
|
|
249
|
-
raw_config, (RTSPSourceConfig, LocalFolderSourceConfig, FtpSourceConfig)
|
|
250
|
-
):
|
|
246
|
+
if isinstance(raw_config, (RTSPSourceConfig, LocalFolderSourceConfig, FtpSourceConfig)):
|
|
251
247
|
return data
|
|
252
248
|
|
|
253
249
|
config_data = raw_config or {}
|
|
@@ -262,7 +258,7 @@ class CameraSourceConfig(BaseModel):
|
|
|
262
258
|
return updated
|
|
263
259
|
|
|
264
260
|
@model_validator(mode="after")
|
|
265
|
-
def _validate_source_config(self) ->
|
|
261
|
+
def _validate_source_config(self) -> CameraSourceConfig:
|
|
266
262
|
match self.type:
|
|
267
263
|
case "rtsp":
|
|
268
264
|
if not isinstance(self.config, RTSPSourceConfig):
|
|
@@ -308,13 +304,13 @@ class Config(BaseModel):
|
|
|
308
304
|
per_camera_alert: dict[str, AlertPolicyOverrides] = Field(default_factory=dict)
|
|
309
305
|
|
|
310
306
|
@model_validator(mode="after")
|
|
311
|
-
def _validate_notifiers(self) ->
|
|
307
|
+
def _validate_notifiers(self) -> Config:
|
|
312
308
|
if not self.notifiers:
|
|
313
309
|
raise ValueError("notifiers must include at least one notifier")
|
|
314
310
|
return self
|
|
315
311
|
|
|
316
312
|
@model_validator(mode="after")
|
|
317
|
-
def _validate_builtin_plugin_configs(self) ->
|
|
313
|
+
def _validate_builtin_plugin_configs(self) -> Config:
|
|
318
314
|
"""Validate built-in plugin configs for early error detection.
|
|
319
315
|
|
|
320
316
|
Third-party plugin configs are validated later during plugin loading.
|
homesec/models/events.py
CHANGED
|
@@ -12,6 +12,7 @@ from homesec.models.filter import FilterResult
|
|
|
12
12
|
|
|
13
13
|
class ClipEvent(BaseModel):
|
|
14
14
|
"""Base class for all clip lifecycle events."""
|
|
15
|
+
|
|
15
16
|
id: int | None = None
|
|
16
17
|
clip_id: str
|
|
17
18
|
timestamp: datetime
|
|
@@ -53,6 +54,7 @@ class ClipRecheckedEvent(ClipEvent):
|
|
|
53
54
|
|
|
54
55
|
class UploadStartedEvent(ClipEvent):
|
|
55
56
|
"""Upload to storage backend started."""
|
|
57
|
+
|
|
56
58
|
event_type: Literal["upload_started"] = "upload_started"
|
|
57
59
|
dest_key: str
|
|
58
60
|
attempt: int
|
|
@@ -60,6 +62,7 @@ class UploadStartedEvent(ClipEvent):
|
|
|
60
62
|
|
|
61
63
|
class UploadCompletedEvent(ClipEvent):
|
|
62
64
|
"""Upload to storage backend completed successfully."""
|
|
65
|
+
|
|
63
66
|
event_type: Literal["upload_completed"] = "upload_completed"
|
|
64
67
|
storage_uri: str
|
|
65
68
|
view_url: str | None
|
|
@@ -69,6 +72,7 @@ class UploadCompletedEvent(ClipEvent):
|
|
|
69
72
|
|
|
70
73
|
class UploadFailedEvent(ClipEvent):
|
|
71
74
|
"""Upload to storage backend failed."""
|
|
75
|
+
|
|
72
76
|
event_type: Literal["upload_failed"] = "upload_failed"
|
|
73
77
|
attempt: int
|
|
74
78
|
error_message: str
|
|
@@ -78,12 +82,14 @@ class UploadFailedEvent(ClipEvent):
|
|
|
78
82
|
|
|
79
83
|
class FilterStartedEvent(ClipEvent):
|
|
80
84
|
"""Object detection filter started."""
|
|
85
|
+
|
|
81
86
|
event_type: Literal["filter_started"] = "filter_started"
|
|
82
87
|
attempt: int
|
|
83
88
|
|
|
84
89
|
|
|
85
90
|
class FilterCompletedEvent(ClipEvent):
|
|
86
91
|
"""Object detection filter completed."""
|
|
92
|
+
|
|
87
93
|
event_type: Literal["filter_completed"] = "filter_completed"
|
|
88
94
|
detected_classes: list[str]
|
|
89
95
|
confidence: float
|
|
@@ -95,6 +101,7 @@ class FilterCompletedEvent(ClipEvent):
|
|
|
95
101
|
|
|
96
102
|
class FilterFailedEvent(ClipEvent):
|
|
97
103
|
"""Object detection filter failed."""
|
|
104
|
+
|
|
98
105
|
event_type: Literal["filter_failed"] = "filter_failed"
|
|
99
106
|
attempt: int
|
|
100
107
|
error_message: str
|
|
@@ -104,12 +111,14 @@ class FilterFailedEvent(ClipEvent):
|
|
|
104
111
|
|
|
105
112
|
class VLMStartedEvent(ClipEvent):
|
|
106
113
|
"""VLM analysis started."""
|
|
114
|
+
|
|
107
115
|
event_type: Literal["vlm_started"] = "vlm_started"
|
|
108
116
|
attempt: int
|
|
109
117
|
|
|
110
118
|
|
|
111
119
|
class VLMCompletedEvent(ClipEvent):
|
|
112
120
|
"""VLM analysis completed."""
|
|
121
|
+
|
|
113
122
|
event_type: Literal["vlm_completed"] = "vlm_completed"
|
|
114
123
|
risk_level: str
|
|
115
124
|
activity_type: str
|
|
@@ -123,6 +132,7 @@ class VLMCompletedEvent(ClipEvent):
|
|
|
123
132
|
|
|
124
133
|
class VLMFailedEvent(ClipEvent):
|
|
125
134
|
"""VLM analysis failed."""
|
|
135
|
+
|
|
126
136
|
event_type: Literal["vlm_failed"] = "vlm_failed"
|
|
127
137
|
attempt: int
|
|
128
138
|
error_message: str
|
|
@@ -132,12 +142,14 @@ class VLMFailedEvent(ClipEvent):
|
|
|
132
142
|
|
|
133
143
|
class VLMSkippedEvent(ClipEvent):
|
|
134
144
|
"""VLM analysis skipped (no trigger classes detected)."""
|
|
145
|
+
|
|
135
146
|
event_type: Literal["vlm_skipped"] = "vlm_skipped"
|
|
136
147
|
reason: str
|
|
137
148
|
|
|
138
149
|
|
|
139
150
|
class AlertDecisionMadeEvent(ClipEvent):
|
|
140
151
|
"""Alert policy decision made."""
|
|
152
|
+
|
|
141
153
|
event_type: Literal["alert_decision_made"] = "alert_decision_made"
|
|
142
154
|
should_notify: bool
|
|
143
155
|
reason: str
|
|
@@ -147,6 +159,7 @@ class AlertDecisionMadeEvent(ClipEvent):
|
|
|
147
159
|
|
|
148
160
|
class NotificationSentEvent(ClipEvent):
|
|
149
161
|
"""Notification sent successfully."""
|
|
162
|
+
|
|
150
163
|
event_type: Literal["notification_sent"] = "notification_sent"
|
|
151
164
|
notifier_name: str
|
|
152
165
|
dedupe_key: str
|
|
@@ -155,6 +168,7 @@ class NotificationSentEvent(ClipEvent):
|
|
|
155
168
|
|
|
156
169
|
class NotificationFailedEvent(ClipEvent):
|
|
157
170
|
"""Notification send failed."""
|
|
171
|
+
|
|
158
172
|
event_type: Literal["notification_failed"] = "notification_failed"
|
|
159
173
|
notifier_name: str
|
|
160
174
|
error_message: str
|
homesec/models/filter.py
CHANGED
homesec/models/vlm.py
CHANGED
homesec/pipeline/core.py
CHANGED
|
@@ -16,8 +16,8 @@ from homesec.models.clip import Clip
|
|
|
16
16
|
from homesec.models.config import Config
|
|
17
17
|
from homesec.models.filter import FilterResult
|
|
18
18
|
from homesec.models.vlm import AnalysisResult
|
|
19
|
-
from homesec.repository import ClipRepository
|
|
20
19
|
from homesec.plugins.notifiers.multiplex import NotifierEntry
|
|
20
|
+
from homesec.repository import ClipRepository
|
|
21
21
|
from homesec.storage_paths import build_clip_path
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
@@ -42,7 +42,7 @@ class UploadOutcome:
|
|
|
42
42
|
|
|
43
43
|
class ClipPipeline:
|
|
44
44
|
"""Orchestrates clip processing through all pipeline stages.
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
Implements error-as-value pattern: stage methods return Result | Error
|
|
47
47
|
instead of raising. This enables partial failures (e.g., upload fails
|
|
48
48
|
but filter+notify still run).
|
|
@@ -56,7 +56,7 @@ class ClipPipeline:
|
|
|
56
56
|
filter_plugin: ObjectFilter,
|
|
57
57
|
vlm_plugin: VLMAnalyzer,
|
|
58
58
|
notifier: Notifier,
|
|
59
|
-
alert_policy:
|
|
59
|
+
alert_policy: AlertPolicy,
|
|
60
60
|
notifier_entries: list[NotifierEntry] | None = None,
|
|
61
61
|
) -> None:
|
|
62
62
|
"""Initialize pipeline with all dependencies."""
|
|
@@ -66,9 +66,7 @@ class ClipPipeline:
|
|
|
66
66
|
self._filter = filter_plugin
|
|
67
67
|
self._vlm = vlm_plugin
|
|
68
68
|
self._notifier = notifier
|
|
69
|
-
self._notifier_entries = self._resolve_notifier_entries(
|
|
70
|
-
notifier, notifier_entries
|
|
71
|
-
)
|
|
69
|
+
self._notifier_entries = self._resolve_notifier_entries(notifier, notifier_entries)
|
|
72
70
|
self._alert_policy = alert_policy
|
|
73
71
|
|
|
74
72
|
# Track in-flight processing
|
|
@@ -79,7 +77,7 @@ class ClipPipeline:
|
|
|
79
77
|
self._sem_upload = asyncio.Semaphore(config.concurrency.upload_workers)
|
|
80
78
|
self._sem_filter = asyncio.Semaphore(config.concurrency.filter_workers)
|
|
81
79
|
self._sem_vlm = asyncio.Semaphore(config.concurrency.vlm_workers)
|
|
82
|
-
|
|
80
|
+
|
|
83
81
|
# Event loop for thread-safe callback handling
|
|
84
82
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
85
83
|
|
|
@@ -95,7 +93,7 @@ class ClipPipeline:
|
|
|
95
93
|
|
|
96
94
|
def set_event_loop(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
97
95
|
"""Set event loop for thread-safe callback handling.
|
|
98
|
-
|
|
96
|
+
|
|
99
97
|
Must be called before registering with ClipSource if source
|
|
100
98
|
runs in a different thread.
|
|
101
99
|
"""
|
|
@@ -119,7 +117,7 @@ class ClipPipeline:
|
|
|
119
117
|
|
|
120
118
|
def on_new_clip(self, clip: Clip) -> None:
|
|
121
119
|
"""Callback for ClipSource when new clip is ready.
|
|
122
|
-
|
|
120
|
+
|
|
123
121
|
Thread-safe: can be called from any thread. Uses stored event loop
|
|
124
122
|
if available, otherwise tries to get current loop.
|
|
125
123
|
"""
|
|
@@ -146,7 +144,7 @@ class ClipPipeline:
|
|
|
146
144
|
|
|
147
145
|
async def _process_clip(self, clip: Clip) -> None:
|
|
148
146
|
"""Process a single clip through all stages.
|
|
149
|
-
|
|
147
|
+
|
|
150
148
|
Flow:
|
|
151
149
|
1. Parallel: upload + filter
|
|
152
150
|
2. Conditional: VLM (if filter detects trigger classes)
|
|
@@ -214,9 +212,7 @@ class ClipPipeline:
|
|
|
214
212
|
analysis_result.activity_type,
|
|
215
213
|
)
|
|
216
214
|
case _:
|
|
217
|
-
raise TypeError(
|
|
218
|
-
f"Unexpected VLM result type: {type(vlm_result).__name__}"
|
|
219
|
-
)
|
|
215
|
+
raise TypeError(f"Unexpected VLM result type: {type(vlm_result).__name__}")
|
|
220
216
|
else:
|
|
221
217
|
await self._repository.record_vlm_skipped(
|
|
222
218
|
clip.clip_id,
|
|
@@ -286,8 +282,7 @@ class ClipPipeline:
|
|
|
286
282
|
op: Callable[[], Awaitable[TResult]],
|
|
287
283
|
on_attempt_start: Callable[[int], Awaitable[None]] | None = None,
|
|
288
284
|
on_attempt_success: Callable[[TResult, int, int], Awaitable[None]] | None = None,
|
|
289
|
-
on_attempt_failure: Callable[[Exception, int, bool, int], Awaitable[None]]
|
|
290
|
-
| None = None,
|
|
285
|
+
on_attempt_failure: Callable[[Exception, int, bool, int], Awaitable[None]] | None = None,
|
|
291
286
|
) -> TResult:
|
|
292
287
|
"""Run stage with retry logic and event emission."""
|
|
293
288
|
max_attempts = max(1, int(self._config.retry.max_attempts))
|
|
@@ -432,9 +427,7 @@ class ClipPipeline:
|
|
|
432
427
|
|
|
433
428
|
async def attempt() -> AnalysisResult:
|
|
434
429
|
async with self._sem_vlm:
|
|
435
|
-
return await self._vlm.analyze(
|
|
436
|
-
clip.local_path, filter_result, self._config.vlm
|
|
437
|
-
)
|
|
430
|
+
return await self._vlm.analyze(clip.local_path, filter_result, self._config.vlm)
|
|
438
431
|
|
|
439
432
|
async def on_attempt_start(attempt_num: int) -> None:
|
|
440
433
|
await self._repository.record_vlm_started(clip.clip_id, attempt=attempt_num)
|
|
@@ -501,10 +494,7 @@ class ClipPipeline:
|
|
|
501
494
|
vlm_failed=vlm_failed,
|
|
502
495
|
)
|
|
503
496
|
|
|
504
|
-
tasks = [
|
|
505
|
-
self._notify_with_entry(entry, alert)
|
|
506
|
-
for entry in self._notifier_entries
|
|
507
|
-
]
|
|
497
|
+
tasks = [self._notify_with_entry(entry, alert) for entry in self._notifier_entries]
|
|
508
498
|
results = await asyncio.gather(*tasks)
|
|
509
499
|
|
|
510
500
|
errors: list[NotifyError] = []
|
|
@@ -515,9 +505,7 @@ class ClipPipeline:
|
|
|
515
505
|
case None:
|
|
516
506
|
continue
|
|
517
507
|
case _:
|
|
518
|
-
raise TypeError(
|
|
519
|
-
f"Unexpected notify result type: {type(result).__name__}"
|
|
520
|
-
)
|
|
508
|
+
raise TypeError(f"Unexpected notify result type: {type(result).__name__}")
|
|
521
509
|
|
|
522
510
|
if errors:
|
|
523
511
|
return errors[0]
|
|
@@ -530,9 +518,7 @@ class ClipPipeline:
|
|
|
530
518
|
) -> None | NotifyError:
|
|
531
519
|
notifier_name = entry.name
|
|
532
520
|
|
|
533
|
-
async def on_attempt_success(
|
|
534
|
-
_result: object, attempt_num: int, _duration_ms: int
|
|
535
|
-
) -> None:
|
|
521
|
+
async def on_attempt_success(_result: object, attempt_num: int, _duration_ms: int) -> None:
|
|
536
522
|
await self._repository.record_notification_sent(
|
|
537
523
|
alert.clip_id,
|
|
538
524
|
notifier_name=notifier_name,
|
|
@@ -609,13 +595,10 @@ class ClipPipeline:
|
|
|
609
595
|
storage_uri = outcome.storage_uri
|
|
610
596
|
view_url = outcome.view_url
|
|
611
597
|
case _:
|
|
612
|
-
raise TypeError(
|
|
613
|
-
f"Unexpected upload result type: {type(upload_result).__name__}"
|
|
614
|
-
)
|
|
598
|
+
raise TypeError(f"Unexpected upload result type: {type(upload_result).__name__}")
|
|
615
599
|
logger.info("Upload complete for %s: %s", clip.clip_id, storage_uri)
|
|
616
600
|
return storage_uri, view_url, False
|
|
617
601
|
|
|
618
|
-
|
|
619
602
|
async def shutdown(self, timeout: float = 30.0) -> None:
|
|
620
603
|
"""Graceful shutdown of pipeline.
|
|
621
604
|
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from dataclasses import dataclass
|
|
7
|
-
from typing import
|
|
8
|
+
from typing import TypeVar
|
|
8
9
|
|
|
9
10
|
from pydantic import BaseModel
|
|
10
11
|
|
|
@@ -14,9 +15,7 @@ from homesec.models.config import AlertPolicyOverrides
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
AlertPolicyFactory = Callable[
|
|
18
|
-
[BaseModel, dict[str, AlertPolicyOverrides], list[str]], AlertPolicy
|
|
19
|
-
]
|
|
18
|
+
AlertPolicyFactory = Callable[[BaseModel, dict[str, AlertPolicyOverrides], list[str]], AlertPolicy]
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
@dataclass(frozen=True)
|
|
@@ -83,8 +83,9 @@ class DefaultAlertPolicy(AlertPolicy):
|
|
|
83
83
|
|
|
84
84
|
# Plugin registration
|
|
85
85
|
from pydantic import BaseModel
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
from homesec.interfaces import AlertPolicy
|
|
88
|
+
from homesec.plugins.alert_policies import AlertPolicyPlugin, alert_policy_plugin
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
@alert_policy_plugin(name="default")
|
|
@@ -94,7 +95,7 @@ def default_alert_policy_plugin() -> AlertPolicyPlugin:
|
|
|
94
95
|
Returns:
|
|
95
96
|
AlertPolicyPlugin for default risk-based alert policy
|
|
96
97
|
"""
|
|
97
|
-
from homesec.models.config import
|
|
98
|
+
from homesec.models.config import DefaultAlertPolicySettings
|
|
98
99
|
|
|
99
100
|
def factory(
|
|
100
101
|
cfg: BaseModel,
|
|
@@ -32,9 +32,8 @@ class NoopAlertPolicy(AlertPolicy):
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
# Plugin registration
|
|
35
|
-
from pydantic import BaseModel
|
|
36
|
-
from homesec.plugins.alert_policies import AlertPolicyPlugin, alert_policy_plugin
|
|
37
35
|
from homesec.interfaces import AlertPolicy
|
|
36
|
+
from homesec.plugins.alert_policies import AlertPolicyPlugin, alert_policy_plugin
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
@alert_policy_plugin(name="noop")
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from dataclasses import dataclass
|
|
7
|
-
from typing import
|
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
8
9
|
|
|
9
10
|
from pydantic import BaseModel
|
|
10
11
|
|
|
@@ -93,9 +94,7 @@ def load_vlm_plugin(config: VLMConfig) -> VLMAnalyzer:
|
|
|
93
94
|
|
|
94
95
|
if plugin_name not in VLM_REGISTRY:
|
|
95
96
|
available = ", ".join(sorted(VLM_REGISTRY.keys()))
|
|
96
|
-
raise ValueError(
|
|
97
|
-
f"Unknown VLM plugin: '{plugin_name}'. Available: {available}"
|
|
98
|
-
)
|
|
97
|
+
raise ValueError(f"Unknown VLM plugin: '{plugin_name}'. Available: {available}")
|
|
99
98
|
|
|
100
99
|
plugin = VLM_REGISTRY[plugin_name]
|
|
101
100
|
|