homesec 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- homesec/__init__.py +20 -0
- homesec/app.py +393 -0
- homesec/cli.py +159 -0
- homesec/config/__init__.py +18 -0
- homesec/config/loader.py +109 -0
- homesec/config/validation.py +82 -0
- homesec/errors.py +71 -0
- homesec/health/__init__.py +5 -0
- homesec/health/server.py +226 -0
- homesec/interfaces.py +249 -0
- homesec/logging_setup.py +176 -0
- homesec/maintenance/__init__.py +1 -0
- homesec/maintenance/cleanup_clips.py +632 -0
- homesec/models/__init__.py +79 -0
- homesec/models/alert.py +32 -0
- homesec/models/clip.py +71 -0
- homesec/models/config.py +362 -0
- homesec/models/events.py +184 -0
- homesec/models/filter.py +62 -0
- homesec/models/source.py +77 -0
- homesec/models/storage.py +12 -0
- homesec/models/vlm.py +99 -0
- homesec/pipeline/__init__.py +6 -0
- homesec/pipeline/alert_policy.py +5 -0
- homesec/pipeline/core.py +639 -0
- homesec/plugins/__init__.py +62 -0
- homesec/plugins/alert_policies/__init__.py +80 -0
- homesec/plugins/alert_policies/default.py +111 -0
- homesec/plugins/alert_policies/noop.py +60 -0
- homesec/plugins/analyzers/__init__.py +126 -0
- homesec/plugins/analyzers/openai.py +446 -0
- homesec/plugins/filters/__init__.py +124 -0
- homesec/plugins/filters/yolo.py +317 -0
- homesec/plugins/notifiers/__init__.py +80 -0
- homesec/plugins/notifiers/mqtt.py +189 -0
- homesec/plugins/notifiers/multiplex.py +106 -0
- homesec/plugins/notifiers/sendgrid_email.py +228 -0
- homesec/plugins/storage/__init__.py +116 -0
- homesec/plugins/storage/dropbox.py +272 -0
- homesec/plugins/storage/local.py +108 -0
- homesec/plugins/utils.py +63 -0
- homesec/py.typed +0 -0
- homesec/repository/__init__.py +5 -0
- homesec/repository/clip_repository.py +552 -0
- homesec/sources/__init__.py +17 -0
- homesec/sources/base.py +224 -0
- homesec/sources/ftp.py +209 -0
- homesec/sources/local_folder.py +238 -0
- homesec/sources/rtsp.py +1251 -0
- homesec/state/__init__.py +10 -0
- homesec/state/postgres.py +501 -0
- homesec/storage_paths.py +46 -0
- homesec/telemetry/__init__.py +0 -0
- homesec/telemetry/db/__init__.py +1 -0
- homesec/telemetry/db/log_table.py +16 -0
- homesec/telemetry/db_log_handler.py +246 -0
- homesec/telemetry/postgres_settings.py +42 -0
- homesec-0.1.0.dist-info/METADATA +446 -0
- homesec-0.1.0.dist-info/RECORD +62 -0
- homesec-0.1.0.dist-info/WHEEL +4 -0
- homesec-0.1.0.dist-info/entry_points.txt +2 -0
- homesec-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Local filesystem storage backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
|
|
10
|
+
from homesec.interfaces import StorageBackend
|
|
11
|
+
from homesec.models.config import LocalStorageConfig
|
|
12
|
+
from homesec.models.storage import StorageUploadResult
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LocalStorage(StorageBackend):
|
|
18
|
+
"""Local storage backend for development and tests."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: LocalStorageConfig) -> None:
|
|
21
|
+
self.root = Path(config.root).expanduser().resolve()
|
|
22
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
self._shutdown_called = False
|
|
24
|
+
|
|
25
|
+
async def put_file(self, local_path: Path, dest_path: str) -> StorageUploadResult:
|
|
26
|
+
self._ensure_open()
|
|
27
|
+
dest = self._full_dest_path(dest_path)
|
|
28
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
await asyncio.to_thread(shutil.copy2, local_path, dest)
|
|
30
|
+
storage_uri = f"local:{dest}"
|
|
31
|
+
view_url = f"file://{dest}"
|
|
32
|
+
return StorageUploadResult(storage_uri=storage_uri, view_url=view_url)
|
|
33
|
+
|
|
34
|
+
async def get_view_url(self, storage_uri: str) -> str | None:
|
|
35
|
+
if not storage_uri.startswith("local:"):
|
|
36
|
+
return None
|
|
37
|
+
return f"file://{storage_uri[6:]}"
|
|
38
|
+
|
|
39
|
+
async def get(self, storage_uri: str, local_path: Path) -> None:
|
|
40
|
+
self._ensure_open()
|
|
41
|
+
src = self._parse_storage_uri(storage_uri)
|
|
42
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
await asyncio.to_thread(shutil.copy2, src, local_path)
|
|
44
|
+
|
|
45
|
+
async def exists(self, storage_uri: str) -> bool:
|
|
46
|
+
self._ensure_open()
|
|
47
|
+
try:
|
|
48
|
+
path = self._parse_storage_uri(storage_uri)
|
|
49
|
+
except ValueError:
|
|
50
|
+
return False
|
|
51
|
+
return await asyncio.to_thread(path.exists)
|
|
52
|
+
|
|
53
|
+
async def delete(self, storage_uri: str) -> None:
|
|
54
|
+
self._ensure_open()
|
|
55
|
+
path = self._parse_storage_uri(storage_uri)
|
|
56
|
+
await asyncio.to_thread(path.unlink, True)
|
|
57
|
+
|
|
58
|
+
async def ping(self) -> bool:
|
|
59
|
+
return self.root.exists() and self.root.is_dir()
|
|
60
|
+
|
|
61
|
+
async def shutdown(self, timeout: float | None = None) -> None:
|
|
62
|
+
_ = timeout
|
|
63
|
+
self._shutdown_called = True
|
|
64
|
+
|
|
65
|
+
def _ensure_open(self) -> None:
|
|
66
|
+
if self._shutdown_called:
|
|
67
|
+
raise RuntimeError("Storage has been shut down")
|
|
68
|
+
|
|
69
|
+
def _parse_storage_uri(self, storage_uri: str) -> Path:
|
|
70
|
+
if not storage_uri.startswith("local:"):
|
|
71
|
+
raise ValueError(f"Invalid storage_uri: {storage_uri}")
|
|
72
|
+
return Path(storage_uri[6:])
|
|
73
|
+
|
|
74
|
+
def _full_dest_path(self, dest_path: str) -> Path:
|
|
75
|
+
cleaned = str(dest_path).lstrip("/")
|
|
76
|
+
if not cleaned or "\\" in cleaned:
|
|
77
|
+
raise ValueError(f"Invalid dest_path: {dest_path}")
|
|
78
|
+
path = PurePosixPath(cleaned)
|
|
79
|
+
if path.is_absolute() or ".." in path.parts:
|
|
80
|
+
raise ValueError(f"Invalid dest_path: {dest_path}")
|
|
81
|
+
return self.root.joinpath(*path.parts)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Plugin registration
|
|
85
|
+
from typing import cast
|
|
86
|
+
from pydantic import BaseModel
|
|
87
|
+
from homesec.plugins.storage import StoragePlugin, storage_plugin
|
|
88
|
+
from homesec.interfaces import StorageBackend
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@storage_plugin(name="local")
|
|
92
|
+
def local_storage_plugin() -> StoragePlugin:
|
|
93
|
+
"""Local storage plugin factory.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
StoragePlugin for local filesystem storage
|
|
97
|
+
"""
|
|
98
|
+
from homesec.models.config import LocalStorageConfig
|
|
99
|
+
|
|
100
|
+
def factory(cfg: BaseModel) -> StorageBackend:
|
|
101
|
+
# Config is already validated by pydantic when loaded
|
|
102
|
+
return LocalStorage(cast(LocalStorageConfig, cfg))
|
|
103
|
+
|
|
104
|
+
return StoragePlugin(
|
|
105
|
+
name="local",
|
|
106
|
+
config_model=LocalStorageConfig,
|
|
107
|
+
factory=factory,
|
|
108
|
+
)
|
homesec/plugins/utils.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Shared utilities for plugin discovery and loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import metadata
|
|
6
|
+
from typing import Iterable, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
PluginT = TypeVar("PluginT")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def iter_entry_points(group: str) -> Iterable[metadata.EntryPoint]:
|
|
12
|
+
"""Iterate entry points (handles Python 3.10+ and earlier).
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
group: Entry point group name (e.g., "homesec.filters")
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Iterable of entry points for the group
|
|
19
|
+
"""
|
|
20
|
+
entry_points = metadata.entry_points()
|
|
21
|
+
if hasattr(entry_points, "select"):
|
|
22
|
+
# Python 3.10+ API
|
|
23
|
+
return entry_points.select(group=group)
|
|
24
|
+
# Python 3.9 API
|
|
25
|
+
return cast(dict[str, list[metadata.EntryPoint]], entry_points).get(group, [])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_plugin_from_entry_point(
|
|
29
|
+
point: metadata.EntryPoint,
|
|
30
|
+
expected_type: type[PluginT],
|
|
31
|
+
plugin_type_name: str,
|
|
32
|
+
) -> PluginT:
|
|
33
|
+
"""Load plugin from entry point.
|
|
34
|
+
|
|
35
|
+
Handles both direct plugin instances and factory callables.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
point: Entry point to load
|
|
39
|
+
expected_type: Expected plugin type (e.g., FilterPlugin)
|
|
40
|
+
plugin_type_name: Human-readable name for error messages (e.g., "Filter")
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Loaded plugin instance
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
TypeError: If entry point doesn't return expected type
|
|
47
|
+
"""
|
|
48
|
+
loaded = point.load()
|
|
49
|
+
|
|
50
|
+
# Direct plugin instance
|
|
51
|
+
if isinstance(loaded, expected_type):
|
|
52
|
+
return loaded
|
|
53
|
+
|
|
54
|
+
# Plugin factory callable
|
|
55
|
+
if callable(loaded):
|
|
56
|
+
result = loaded()
|
|
57
|
+
if isinstance(result, expected_type):
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
raise TypeError(
|
|
61
|
+
f"Invalid {plugin_type_name} plugin entry point: {point.name}. "
|
|
62
|
+
f"Expected {expected_type.__name__} or callable returning it."
|
|
63
|
+
)
|
homesec/py.typed
ADDED
|
File without changes
|