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
homesec/logging_setup.py
ADDED
|
@@ -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."""
|