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,176 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import logging.config
6
+ import os
7
+
8
+ from homesec.telemetry.postgres_settings import PostgresConfig
9
+
10
+ _CURRENT_CAMERA_NAME = "-"
11
+ _CURRENT_RECORDING_ID: str | None = None
12
+ _STANDARD_LOGRECORD_ATTRS = {
13
+ "name",
14
+ "msg",
15
+ "message",
16
+ "asctime",
17
+ "args",
18
+ "levelname",
19
+ "levelno",
20
+ "pathname",
21
+ "filename",
22
+ "module",
23
+ "exc_info",
24
+ "exc_text",
25
+ "stack_info",
26
+ "lineno",
27
+ "funcName",
28
+ "created",
29
+ "msecs",
30
+ "relativeCreated",
31
+ "thread",
32
+ "threadName",
33
+ "processName",
34
+ "process",
35
+ "taskName",
36
+ }
37
+
38
+ class _CameraNameFilter(logging.Filter):
39
+ def filter(self, record: logging.LogRecord) -> bool:
40
+ if not hasattr(record, "camera_name") or getattr(record, "camera_name") in (None, ""):
41
+ record.camera_name = _CURRENT_CAMERA_NAME
42
+ return True
43
+
44
+
45
+ class _RecordingIdFilter(logging.Filter):
46
+ def filter(self, record: logging.LogRecord) -> bool:
47
+ if not hasattr(record, "recording_id") or getattr(record, "recording_id") in (None, ""):
48
+ record.recording_id = _CURRENT_RECORDING_ID
49
+ return True
50
+
51
+
52
+ class _DbLevelFilter(logging.Filter):
53
+ def __init__(self, *, min_level: int) -> None:
54
+ super().__init__()
55
+ self._min_level = min_level
56
+
57
+ def filter(self, record: logging.LogRecord) -> bool:
58
+ if getattr(record, "kind", None) == "event":
59
+ return True
60
+ return record.levelno >= logging.WARNING or record.levelno >= self._min_level
61
+
62
+
63
+ class _JsonExtraFormatter(logging.Formatter):
64
+ def format(self, record: logging.LogRecord) -> str:
65
+ base = super().format(record)
66
+ extras = _extract_extras(record)
67
+ if not extras:
68
+ return base
69
+ extras_json = json.dumps(extras, indent=2, default=str, sort_keys=True)
70
+ return f"{base}\n{extras_json}"
71
+
72
+
73
+ def _extract_extras(record: logging.LogRecord) -> dict[str, object]:
74
+ extras: dict[str, object] = {}
75
+ for key, value in record.__dict__.items():
76
+ if key in _STANDARD_LOGRECORD_ATTRS:
77
+ continue
78
+ if key in {"camera_name", "recording_id"}:
79
+ continue
80
+ extras[key] = value
81
+ return extras
82
+
83
+
84
+ def set_camera_name(name: str | None) -> None:
85
+ """Set the `camera_name` value injected into log records."""
86
+ global _CURRENT_CAMERA_NAME
87
+ _CURRENT_CAMERA_NAME = name or "-"
88
+
89
+ def set_recording_id(recording_id: str | None) -> None:
90
+ """Set the `recording_id` value injected into log records."""
91
+ global _CURRENT_RECORDING_ID
92
+ _CURRENT_RECORDING_ID = recording_id or None
93
+
94
+
95
+ def _install_camera_filter() -> None:
96
+ root = logging.getLogger()
97
+ for handler in root.handlers:
98
+ if any(isinstance(f, _CameraNameFilter) for f in handler.filters):
99
+ continue
100
+ handler.addFilter(_CameraNameFilter())
101
+
102
+ def _install_recording_filter() -> None:
103
+ root = logging.getLogger()
104
+ for handler in root.handlers:
105
+ if any(isinstance(f, _RecordingIdFilter) for f in handler.filters):
106
+ continue
107
+ handler.addFilter(_RecordingIdFilter())
108
+
109
+
110
+ def configure_logging(*, log_level: str = "INFO", camera_name: str | None = None) -> None:
111
+ """Configure root logging with a consistent format.
112
+
113
+ Format includes `camera_name` plus `module:lineno` for easier multi-process debugging.
114
+ If `DB_DSN` is configured (via env or `.env`), also emits logs to Postgres as JSON.
115
+ """
116
+ console_level_name = str(log_level).upper()
117
+ default_console_fmt = (
118
+ "%(asctime)s %(levelname)s [%(camera_name)s] "
119
+ "%(module)s %(pathname)s:%(lineno)d %(message)s"
120
+ )
121
+ console_fmt = os.getenv("CONSOLE_LOG_FORMAT", default_console_fmt)
122
+
123
+ logging.config.dictConfig(
124
+ {
125
+ "version": 1,
126
+ "disable_existing_loggers": False,
127
+ "formatters": {
128
+ "default": {
129
+ "()": "homesec.logging_setup._JsonExtraFormatter",
130
+ "format": console_fmt,
131
+ }
132
+ },
133
+ "handlers": {
134
+ "console": {
135
+ "class": "logging.StreamHandler",
136
+ "level": console_level_name,
137
+ "formatter": "default",
138
+ "stream": "ext://sys.stdout",
139
+ }
140
+ },
141
+ "root": {"level": "DEBUG", "handlers": ["console"]},
142
+ }
143
+ )
144
+
145
+ _install_camera_filter()
146
+ _install_recording_filter()
147
+ set_camera_name(camera_name)
148
+ logging.captureWarnings(True)
149
+
150
+ # Reduce noisy third-party request logs by default.
151
+ logging.getLogger("dropbox").setLevel(logging.WARNING)
152
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
153
+
154
+ config = PostgresConfig()
155
+ if not config.enabled:
156
+ return
157
+
158
+ root = logging.getLogger()
159
+ try:
160
+ from homesec.telemetry.db_log_handler import AsyncPostgresJsonLogHandler
161
+ except Exception as exc:
162
+ # Keep the app running even if DB logging deps aren't installed.
163
+ logging.getLogger(__name__).warning("DB_DSN is set but DB log handler failed to import: %s", exc)
164
+ return
165
+
166
+ for handler in list(root.handlers):
167
+ if isinstance(handler, AsyncPostgresJsonLogHandler):
168
+ return
169
+
170
+ db_handler = AsyncPostgresJsonLogHandler(config)
171
+ min_level = getattr(logging, config.db_log_level, logging.INFO)
172
+ db_handler.setLevel(logging.DEBUG)
173
+ db_handler.addFilter(_DbLevelFilter(min_level=min_level))
174
+ root.addHandler(db_handler)
175
+ _install_camera_filter()
176
+ _install_recording_filter()
@@ -0,0 +1 @@
1
+ """Maintenance and administrative workflows for HomeSec."""