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.
Files changed (62) hide show
  1. homesec/__init__.py +20 -0
  2. homesec/app.py +393 -0
  3. homesec/cli.py +159 -0
  4. homesec/config/__init__.py +18 -0
  5. homesec/config/loader.py +109 -0
  6. homesec/config/validation.py +82 -0
  7. homesec/errors.py +71 -0
  8. homesec/health/__init__.py +5 -0
  9. homesec/health/server.py +226 -0
  10. homesec/interfaces.py +249 -0
  11. homesec/logging_setup.py +176 -0
  12. homesec/maintenance/__init__.py +1 -0
  13. homesec/maintenance/cleanup_clips.py +632 -0
  14. homesec/models/__init__.py +79 -0
  15. homesec/models/alert.py +32 -0
  16. homesec/models/clip.py +71 -0
  17. homesec/models/config.py +362 -0
  18. homesec/models/events.py +184 -0
  19. homesec/models/filter.py +62 -0
  20. homesec/models/source.py +77 -0
  21. homesec/models/storage.py +12 -0
  22. homesec/models/vlm.py +99 -0
  23. homesec/pipeline/__init__.py +6 -0
  24. homesec/pipeline/alert_policy.py +5 -0
  25. homesec/pipeline/core.py +639 -0
  26. homesec/plugins/__init__.py +62 -0
  27. homesec/plugins/alert_policies/__init__.py +80 -0
  28. homesec/plugins/alert_policies/default.py +111 -0
  29. homesec/plugins/alert_policies/noop.py +60 -0
  30. homesec/plugins/analyzers/__init__.py +126 -0
  31. homesec/plugins/analyzers/openai.py +446 -0
  32. homesec/plugins/filters/__init__.py +124 -0
  33. homesec/plugins/filters/yolo.py +317 -0
  34. homesec/plugins/notifiers/__init__.py +80 -0
  35. homesec/plugins/notifiers/mqtt.py +189 -0
  36. homesec/plugins/notifiers/multiplex.py +106 -0
  37. homesec/plugins/notifiers/sendgrid_email.py +228 -0
  38. homesec/plugins/storage/__init__.py +116 -0
  39. homesec/plugins/storage/dropbox.py +272 -0
  40. homesec/plugins/storage/local.py +108 -0
  41. homesec/plugins/utils.py +63 -0
  42. homesec/py.typed +0 -0
  43. homesec/repository/__init__.py +5 -0
  44. homesec/repository/clip_repository.py +552 -0
  45. homesec/sources/__init__.py +17 -0
  46. homesec/sources/base.py +224 -0
  47. homesec/sources/ftp.py +209 -0
  48. homesec/sources/local_folder.py +238 -0
  49. homesec/sources/rtsp.py +1251 -0
  50. homesec/state/__init__.py +10 -0
  51. homesec/state/postgres.py +501 -0
  52. homesec/storage_paths.py +46 -0
  53. homesec/telemetry/__init__.py +0 -0
  54. homesec/telemetry/db/__init__.py +1 -0
  55. homesec/telemetry/db/log_table.py +16 -0
  56. homesec/telemetry/db_log_handler.py +246 -0
  57. homesec/telemetry/postgres_settings.py +42 -0
  58. homesec-0.1.0.dist-info/METADATA +446 -0
  59. homesec-0.1.0.dist-info/RECORD +62 -0
  60. homesec-0.1.0.dist-info/WHEEL +4 -0
  61. homesec-0.1.0.dist-info/entry_points.txt +2 -0
  62. 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
+ )
@@ -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
@@ -0,0 +1,5 @@
1
+ """Repository layer for coordinating state + event persistence."""
2
+
3
+ from homesec.repository.clip_repository import ClipRepository
4
+
5
+ __all__ = ["ClipRepository"]