pyzm 2.3.4__tar.gz → 2.3.6__tar.gz
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.
- {pyzm-2.3.4/pyzm.egg-info → pyzm-2.3.6}/PKG-INFO +1 -1
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/__init__.py +1 -1
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/log.py +41 -6
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/models/config.py +1 -1
- {pyzm-2.3.4 → pyzm-2.3.6/pyzm.egg-info}/PKG-INFO +1 -1
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_config_variants.py +1 -1
- pyzm-2.3.6/tests/test_face_dlib_lock.py +44 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_log.py +187 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_models/test_config.py +1 -1
- pyzm-2.3.4/tests/test_face_dlib_lock.py +0 -42
- {pyzm-2.3.4 → pyzm-2.3.6}/LICENSE +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/MANIFEST.in +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/README.md +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyproject.toml +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/client.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/alpr.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/base.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/birdnet.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/coral.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/face_dlib.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/face_tpu.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/rekognition.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/yolo.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/yolo_darknet.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/backends/yolo_onnx.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/detector.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/face_train_dlib.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/filters.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/ml/pipeline.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/models/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/models/detection.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/models/zm.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/serve/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/serve/__main__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/serve/app.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/serve/auth.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/__main__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/_import_panel.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/_review_panel.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/_train_panel.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/app.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/assets/pyzm.png +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/auto_label.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/dataset.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/local_import.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/pipeline.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/trainer.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/train/verification.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/zm/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/zm/api.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/zm/auth.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/zm/db.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/zm/media.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm/zm/shm.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm.egg-info/SOURCES.txt +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm.egg-info/dependency_links.txt +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm.egg-info/requires.txt +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/pyzm.egg-info/top_level.txt +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/setup.cfg +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/setup.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/conftest.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_config.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_filters.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml/test_birdnet.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml/test_detector.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml/test_filters.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml/test_pipeline.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml/test_pipeline_lazy.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml/test_yolo_onnx.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/conftest.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_basic_detection.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_disabled_models.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_edge_cases.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_filter_combinations.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_frame_strategies.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_from_dict.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_match_strategies.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_min_confidence.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_model_dimensions.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_model_discovery.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_multi_model.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_past_detections.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_pattern_filtering.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_pipeline_loading.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_pre_existing_labels.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_remote_serve.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_server_config.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_size_filtering.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_stream_config.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_ml_e2e/test_zone_filtering.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_models/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_models/test_detection.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_models/test_zm.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_serve/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_serve/test_app.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_serve/test_auth.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_serve/test_models_all.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_train/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_train/test_app_review.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_train/test_auto_label.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_train/test_dataset.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_train/test_local_import.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_train/test_trainer.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_train/test_verification.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_train_pipeline.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm/test_api.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm/test_auth.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm/test_client.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/__init__.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/conftest.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_auth.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_configs.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_db.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_detection.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_events.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_frames.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_monitors.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_states.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_system.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_write_events.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_write_monitors.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_write_notes.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_write_state.py +0 -0
- {pyzm-2.3.4 → pyzm-2.3.6}/tests/test_zm_e2e/test_zones.py +0 -0
|
@@ -22,7 +22,12 @@ import sys as _sys
|
|
|
22
22
|
import time
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
-
__all__ = [
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ZMLogAdapter",
|
|
27
|
+
"setup_zm_logging",
|
|
28
|
+
"get_logpath",
|
|
29
|
+
"get_log_file",
|
|
30
|
+
]
|
|
26
31
|
|
|
27
32
|
# ZM level mapping: DBG=1, INF=0, WAR=-1, ERR=-2, FAT=-3
|
|
28
33
|
_ZM_LEVELS: dict[str, int] = {"DBG": 1, "INF": 0, "WAR": -1, "ERR": -2, "FAT": -3}
|
|
@@ -467,11 +472,6 @@ def setup_zm_logging(
|
|
|
467
472
|
"dump_console": os.environ.get("PYZM_DUMPCONSOLE"),
|
|
468
473
|
}
|
|
469
474
|
|
|
470
|
-
# Apply defaults for None values
|
|
471
|
-
for key, val in defaults.items():
|
|
472
|
-
if config.get(key) is None and val is not None:
|
|
473
|
-
config[key] = val
|
|
474
|
-
|
|
475
475
|
# Round 1 overrides (before DB read)
|
|
476
476
|
for key in override:
|
|
477
477
|
if override[key]:
|
|
@@ -648,3 +648,38 @@ def setup_zm_logging(
|
|
|
648
648
|
pass
|
|
649
649
|
|
|
650
650
|
return adapter
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def get_logpath(conf_path: str = "/etc/zm") -> str:
|
|
654
|
+
"""Return the resolved ZM log directory without initializing logging.
|
|
655
|
+
|
|
656
|
+
Precedence (matches setup_zm_logging):
|
|
657
|
+
PYZM_LOGPATH env var
|
|
658
|
+
> ZM_PATH_LOGS in /etc/zm/zm.conf and conf.d/*.conf
|
|
659
|
+
> /var/log/zm (default)
|
|
660
|
+
|
|
661
|
+
Safe to call from any process; does not touch the DB, open handlers,
|
|
662
|
+
or register signal handlers.
|
|
663
|
+
"""
|
|
664
|
+
env = os.environ.get("PYZM_LOGPATH")
|
|
665
|
+
if env:
|
|
666
|
+
return env
|
|
667
|
+
conf_val = _read_zm_conf_full(conf_path).get("logpath")
|
|
668
|
+
if conf_val:
|
|
669
|
+
return conf_val
|
|
670
|
+
return "/var/log/zm"
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def get_log_file() -> str | None:
|
|
674
|
+
"""Return the file path pyzm is currently writing logs to, or None.
|
|
675
|
+
|
|
676
|
+
Returns None when file logging is disabled (``ZM_LOG_LEVEL_FILE``
|
|
677
|
+
set to off) or when :func:`setup_zm_logging` has not been called.
|
|
678
|
+
Reflects live state -- honors ``ZM_LOG_DEBUG_FILE`` overrides and
|
|
679
|
+
any post-SIGHUP rotation, since it reads ``baseFilename`` off the
|
|
680
|
+
actual ``WatchedFileHandler``.
|
|
681
|
+
"""
|
|
682
|
+
for h in logging.getLogger("pyzm").handlers:
|
|
683
|
+
if isinstance(h, logging.handlers.WatchedFileHandler):
|
|
684
|
+
return h.baseFilename
|
|
685
|
+
return None
|
|
@@ -447,7 +447,7 @@ class StreamConfig(BaseModel):
|
|
|
447
447
|
save_frames: bool = False
|
|
448
448
|
save_frames_dir: str = "/tmp"
|
|
449
449
|
delete_after_analyze: bool = False
|
|
450
|
-
convert_snapshot_to_fid: bool =
|
|
450
|
+
convert_snapshot_to_fid: bool = False
|
|
451
451
|
|
|
452
452
|
@classmethod
|
|
453
453
|
def from_dict(cls, d: dict[str, Any]) -> "StreamConfig":
|
|
@@ -661,7 +661,7 @@ class TestStreamConfigVariants:
|
|
|
661
661
|
sc = StreamConfig.from_dict({})
|
|
662
662
|
assert sc.resize is None
|
|
663
663
|
assert sc.frame_set == ["snapshot", "alarm", "1"]
|
|
664
|
-
assert sc.convert_snapshot_to_fid is
|
|
664
|
+
assert sc.convert_snapshot_to_fid is False
|
|
665
665
|
assert sc.save_frames is False
|
|
666
666
|
|
|
667
667
|
def test_download_bool_string(self):
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from pyzm.models.config import ModelConfig, ModelFramework, ModelType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestFaceDlibLockRelease(unittest.TestCase):
|
|
9
|
+
"""H4: Lock must be released even if face_recognition raises."""
|
|
10
|
+
|
|
11
|
+
def test_lock_released_on_exception(self):
|
|
12
|
+
# Inject mock modules for the lazy imports inside detect().
|
|
13
|
+
# Use patch.dict so cleanup restores sys.modules and the mocks
|
|
14
|
+
# don't leak into other tests that genuinely need cv2.
|
|
15
|
+
mock_cv2 = MagicMock()
|
|
16
|
+
mock_fr = MagicMock()
|
|
17
|
+
mock_imutils = MagicMock()
|
|
18
|
+
|
|
19
|
+
with patch.dict("sys.modules", {
|
|
20
|
+
"cv2": mock_cv2,
|
|
21
|
+
"imutils": mock_imutils,
|
|
22
|
+
"face_recognition": mock_fr,
|
|
23
|
+
}):
|
|
24
|
+
from pyzm.ml.backends.face_dlib import FaceDlibBackend
|
|
25
|
+
|
|
26
|
+
config = ModelConfig(
|
|
27
|
+
type=ModelType.FACE,
|
|
28
|
+
framework=ModelFramework.FACE_DLIB,
|
|
29
|
+
known_faces_dir="/tmp/known",
|
|
30
|
+
)
|
|
31
|
+
backend = FaceDlibBackend(config)
|
|
32
|
+
backend._knn = MagicMock() # pretend loaded
|
|
33
|
+
backend._auto_lock = True
|
|
34
|
+
backend.acquire_lock = MagicMock()
|
|
35
|
+
backend.release_lock = MagicMock()
|
|
36
|
+
|
|
37
|
+
mock_fr.face_locations.side_effect = RuntimeError("GPU OOM")
|
|
38
|
+
|
|
39
|
+
dummy = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
40
|
+
with self.assertRaises(RuntimeError):
|
|
41
|
+
backend.detect(dummy)
|
|
42
|
+
|
|
43
|
+
backend.acquire_lock.assert_called_once()
|
|
44
|
+
backend.release_lock.assert_called_once()
|
|
@@ -14,6 +14,7 @@ from pyzm.log import (
|
|
|
14
14
|
_read_zm_conf_full, _zm_config_to_handler_level, _ZM_OFF,
|
|
15
15
|
_ZMDBHandler, _ZMFileFormatter, _ZMSyslogFormatter,
|
|
16
16
|
ZMLogAdapter, setup_zm_logging,
|
|
17
|
+
get_logpath, get_log_file,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
@@ -668,6 +669,73 @@ class TestSetupZmLogging:
|
|
|
668
669
|
assert adapter._config["log_debug_target"] == "zmc_m1" # DB value kept (no override)
|
|
669
670
|
adapter.close()
|
|
670
671
|
|
|
672
|
+
@patch("pyzm.log._read_zm_db_log_config", return_value={})
|
|
673
|
+
@patch("pyzm.log._read_zm_conf_full", return_value={
|
|
674
|
+
"dbuser": "u", "dbpassword": "p", "dbhost": "h",
|
|
675
|
+
"dbname": "zm", "webuser": "zmuser", "webgroup": "zmgroup",
|
|
676
|
+
"logpath": "/var/log/zoneminder",
|
|
677
|
+
})
|
|
678
|
+
@patch("pyzm.log._signal.signal")
|
|
679
|
+
def test_conf_logpath_honored(self, mock_sig, mock_conf, mock_db, monkeypatch):
|
|
680
|
+
"""ZM_PATH_LOGS from zm.conf must flow through when no env/override is set.
|
|
681
|
+
|
|
682
|
+
Regression test for #45 -- the first defaults loop used to populate
|
|
683
|
+
config['logpath'] with '/var/log/zm' before the conf was read, causing
|
|
684
|
+
the conf value to be silently dropped.
|
|
685
|
+
"""
|
|
686
|
+
monkeypatch.delenv("PYZM_LOGPATH", raising=False)
|
|
687
|
+
monkeypatch.delenv("PYZM_WEBUSER", raising=False)
|
|
688
|
+
monkeypatch.delenv("PYZM_WEBGROUP", raising=False)
|
|
689
|
+
adapter = setup_zm_logging(name="test_conf_logpath", override={
|
|
690
|
+
"log_level_file": _ZM_OFF,
|
|
691
|
+
"log_level_db": _ZM_OFF,
|
|
692
|
+
"log_level_syslog": _ZM_OFF,
|
|
693
|
+
})
|
|
694
|
+
assert adapter._config["logpath"] == "/var/log/zoneminder"
|
|
695
|
+
assert adapter._config["webuser"] == "zmuser"
|
|
696
|
+
assert adapter._config["webgroup"] == "zmgroup"
|
|
697
|
+
adapter.close()
|
|
698
|
+
|
|
699
|
+
@patch("pyzm.log._read_zm_db_log_config", return_value={})
|
|
700
|
+
@patch("pyzm.log._read_zm_conf_full", return_value={
|
|
701
|
+
"dbuser": "u", "dbpassword": "p", "dbhost": "h",
|
|
702
|
+
"dbname": "zm", "webuser": "zmuser", "webgroup": "zmgroup",
|
|
703
|
+
"logpath": "/var/log/zoneminder",
|
|
704
|
+
})
|
|
705
|
+
@patch("pyzm.log._signal.signal")
|
|
706
|
+
def test_env_overrides_conf_logpath(self, mock_sig, mock_conf, mock_db, monkeypatch):
|
|
707
|
+
"""PYZM_LOGPATH env var still wins over the conf-file value."""
|
|
708
|
+
monkeypatch.setenv("PYZM_LOGPATH", "/env/logs")
|
|
709
|
+
adapter = setup_zm_logging(name="test_env_logpath", override={
|
|
710
|
+
"log_level_file": _ZM_OFF,
|
|
711
|
+
"log_level_db": _ZM_OFF,
|
|
712
|
+
"log_level_syslog": _ZM_OFF,
|
|
713
|
+
})
|
|
714
|
+
assert adapter._config["logpath"] == "/env/logs"
|
|
715
|
+
adapter.close()
|
|
716
|
+
|
|
717
|
+
@patch("pyzm.log._read_zm_db_log_config", return_value={})
|
|
718
|
+
@patch("pyzm.log._read_zm_conf_full", return_value={
|
|
719
|
+
"dbuser": "u", "dbpassword": "p", "dbhost": "h",
|
|
720
|
+
"dbname": "zm", "webuser": None, "webgroup": None,
|
|
721
|
+
"logpath": None,
|
|
722
|
+
})
|
|
723
|
+
@patch("pyzm.log._signal.signal")
|
|
724
|
+
def test_final_defaults_when_no_source(self, mock_sig, mock_conf, mock_db, monkeypatch):
|
|
725
|
+
"""Hard-coded defaults apply only when env, conf, and override are all empty."""
|
|
726
|
+
monkeypatch.delenv("PYZM_LOGPATH", raising=False)
|
|
727
|
+
monkeypatch.delenv("PYZM_WEBUSER", raising=False)
|
|
728
|
+
monkeypatch.delenv("PYZM_WEBGROUP", raising=False)
|
|
729
|
+
adapter = setup_zm_logging(name="test_default_logpath", override={
|
|
730
|
+
"log_level_file": _ZM_OFF,
|
|
731
|
+
"log_level_db": _ZM_OFF,
|
|
732
|
+
"log_level_syslog": _ZM_OFF,
|
|
733
|
+
})
|
|
734
|
+
assert adapter._config["logpath"] == "/var/log/zm"
|
|
735
|
+
assert adapter._config["webuser"] == "www-data"
|
|
736
|
+
assert adapter._config["webgroup"] == "www-data"
|
|
737
|
+
adapter.close()
|
|
738
|
+
|
|
671
739
|
@patch("pyzm.log._read_zm_db_log_config", return_value={})
|
|
672
740
|
@patch("pyzm.log._read_zm_conf_full", return_value={
|
|
673
741
|
"dbuser": "u", "dbpassword": "p", "dbhost": "h",
|
|
@@ -933,3 +1001,122 @@ class TestFileFormatAllLevels:
|
|
|
933
1001
|
for lvl in range(1, 10):
|
|
934
1002
|
result = self._format(logging.DEBUG, zm_debug_level=lvl)
|
|
935
1003
|
assert f"].DB{lvl} [" in result
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
# ===================================================================
|
|
1007
|
+
# TestGetLogpath
|
|
1008
|
+
# ===================================================================
|
|
1009
|
+
|
|
1010
|
+
class TestGetLogpath:
|
|
1011
|
+
"""Tests for the public get_logpath() helper (issue #46)."""
|
|
1012
|
+
|
|
1013
|
+
def test_env_var_wins(self, tmp_path, monkeypatch):
|
|
1014
|
+
"""PYZM_LOGPATH beats conf-file value."""
|
|
1015
|
+
(tmp_path / "zm.conf").write_text("ZM_PATH_LOGS=/conf/path\n")
|
|
1016
|
+
monkeypatch.setenv("PYZM_LOGPATH", "/env/path")
|
|
1017
|
+
assert get_logpath(str(tmp_path)) == "/env/path"
|
|
1018
|
+
|
|
1019
|
+
def test_conf_file_wins_over_default(self, tmp_path, monkeypatch):
|
|
1020
|
+
"""ZM_PATH_LOGS in zm.conf is used when env is unset."""
|
|
1021
|
+
(tmp_path / "zm.conf").write_text("ZM_PATH_LOGS=/conf/path\n")
|
|
1022
|
+
monkeypatch.delenv("PYZM_LOGPATH", raising=False)
|
|
1023
|
+
assert get_logpath(str(tmp_path)) == "/conf/path"
|
|
1024
|
+
|
|
1025
|
+
def test_default_when_nothing_set(self, tmp_path, monkeypatch):
|
|
1026
|
+
"""Falls back to /var/log/zm when env and conf are silent."""
|
|
1027
|
+
(tmp_path / "zm.conf").write_text("")
|
|
1028
|
+
monkeypatch.delenv("PYZM_LOGPATH", raising=False)
|
|
1029
|
+
assert get_logpath(str(tmp_path)) == "/var/log/zm"
|
|
1030
|
+
|
|
1031
|
+
def test_does_not_require_setup(self, tmp_path, monkeypatch):
|
|
1032
|
+
"""get_logpath() works standalone -- no setup_zm_logging needed."""
|
|
1033
|
+
(tmp_path / "zm.conf").write_text("ZM_PATH_LOGS=/standalone/path\n")
|
|
1034
|
+
monkeypatch.delenv("PYZM_LOGPATH", raising=False)
|
|
1035
|
+
# Wipe any pyzm logger handlers to prove we don't peek at them
|
|
1036
|
+
logging.getLogger("pyzm").handlers.clear()
|
|
1037
|
+
assert get_logpath(str(tmp_path)) == "/standalone/path"
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
# ===================================================================
|
|
1041
|
+
# TestGetLogFile
|
|
1042
|
+
# ===================================================================
|
|
1043
|
+
|
|
1044
|
+
class TestGetLogFile:
|
|
1045
|
+
"""Tests for the public get_log_file() helper (issue #46)."""
|
|
1046
|
+
|
|
1047
|
+
def setup_method(self):
|
|
1048
|
+
# Each test starts from a clean pyzm logger
|
|
1049
|
+
logging.getLogger("pyzm").handlers.clear()
|
|
1050
|
+
|
|
1051
|
+
def teardown_method(self):
|
|
1052
|
+
logging.getLogger("pyzm").handlers.clear()
|
|
1053
|
+
|
|
1054
|
+
def test_returns_none_when_no_handler(self):
|
|
1055
|
+
"""Before setup_zm_logging -- no file handler attached -- returns None."""
|
|
1056
|
+
assert get_log_file() is None
|
|
1057
|
+
|
|
1058
|
+
@patch("pyzm.log._read_zm_db_log_config", return_value={})
|
|
1059
|
+
@patch("pyzm.log._read_zm_conf_full", return_value={
|
|
1060
|
+
"dbuser": "u", "dbpassword": "p", "dbhost": "h",
|
|
1061
|
+
"dbname": "zm", "webuser": "www", "webgroup": "www",
|
|
1062
|
+
"logpath": None, # forces final-default fallback
|
|
1063
|
+
})
|
|
1064
|
+
@patch("pyzm.log._signal.signal")
|
|
1065
|
+
def test_returns_path_after_setup(self, mock_sig, mock_conf, mock_db, tmp_path):
|
|
1066
|
+
"""Returns the WatchedFileHandler's open file path after setup."""
|
|
1067
|
+
adapter = setup_zm_logging(name="zmesdetect_m1", override={
|
|
1068
|
+
"logpath": str(tmp_path),
|
|
1069
|
+
"log_level_file": 0,
|
|
1070
|
+
"log_level_db": _ZM_OFF,
|
|
1071
|
+
"log_level_syslog": _ZM_OFF,
|
|
1072
|
+
})
|
|
1073
|
+
try:
|
|
1074
|
+
assert get_log_file() == str(tmp_path / "zmesdetect_m1.log")
|
|
1075
|
+
finally:
|
|
1076
|
+
adapter.close()
|
|
1077
|
+
|
|
1078
|
+
@patch("pyzm.log._read_zm_db_log_config", return_value={})
|
|
1079
|
+
@patch("pyzm.log._read_zm_conf_full", return_value={
|
|
1080
|
+
"dbuser": "u", "dbpassword": "p", "dbhost": "h",
|
|
1081
|
+
"dbname": "zm", "webuser": "www", "webgroup": "www",
|
|
1082
|
+
"logpath": "/tmp",
|
|
1083
|
+
})
|
|
1084
|
+
@patch("pyzm.log._signal.signal")
|
|
1085
|
+
def test_returns_none_when_file_logging_disabled(
|
|
1086
|
+
self, mock_sig, mock_conf, mock_db,
|
|
1087
|
+
):
|
|
1088
|
+
"""File handler isn't attached when log_level_file == _ZM_OFF."""
|
|
1089
|
+
adapter = setup_zm_logging(name="zm_nofile", override={
|
|
1090
|
+
"log_level_file": _ZM_OFF,
|
|
1091
|
+
"log_level_db": _ZM_OFF,
|
|
1092
|
+
"log_level_syslog": _ZM_OFF,
|
|
1093
|
+
})
|
|
1094
|
+
try:
|
|
1095
|
+
assert get_log_file() is None
|
|
1096
|
+
finally:
|
|
1097
|
+
adapter.close()
|
|
1098
|
+
|
|
1099
|
+
@patch("pyzm.log._read_zm_db_log_config", return_value={})
|
|
1100
|
+
@patch("pyzm.log._read_zm_conf_full", return_value={
|
|
1101
|
+
"dbuser": "u", "dbpassword": "p", "dbhost": "h",
|
|
1102
|
+
"dbname": "zm", "webuser": "www", "webgroup": "www",
|
|
1103
|
+
"logpath": "/should/be/ignored",
|
|
1104
|
+
})
|
|
1105
|
+
@patch("pyzm.log._signal.signal")
|
|
1106
|
+
def test_reflects_debug_file_override(
|
|
1107
|
+
self, mock_sig, mock_conf, mock_db, tmp_path,
|
|
1108
|
+
):
|
|
1109
|
+
"""ZM_LOG_DEBUG_FILE override is visible in get_log_file()."""
|
|
1110
|
+
debug_file = tmp_path / "custom-debug.log"
|
|
1111
|
+
adapter = setup_zm_logging(name="zm_dbg", override={
|
|
1112
|
+
"log_debug": 1,
|
|
1113
|
+
"log_level_debug": 5,
|
|
1114
|
+
"log_debug_file": str(debug_file),
|
|
1115
|
+
"log_level_file": 0,
|
|
1116
|
+
"log_level_db": _ZM_OFF,
|
|
1117
|
+
"log_level_syslog": _ZM_OFF,
|
|
1118
|
+
})
|
|
1119
|
+
try:
|
|
1120
|
+
assert get_log_file() == str(debug_file)
|
|
1121
|
+
finally:
|
|
1122
|
+
adapter.close()
|
|
@@ -604,7 +604,7 @@ class TestStreamConfig:
|
|
|
604
604
|
assert sc.sleep_between_attempts == 3
|
|
605
605
|
assert sc.save_frames is False
|
|
606
606
|
assert sc.save_frames_dir == "/tmp"
|
|
607
|
-
assert sc.convert_snapshot_to_fid is
|
|
607
|
+
assert sc.convert_snapshot_to_fid is False
|
|
608
608
|
|
|
609
609
|
def test_all_fields_override(self):
|
|
610
610
|
sc = StreamConfig(
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import unittest
|
|
3
|
-
from unittest.mock import MagicMock
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
from pyzm.models.config import ModelConfig, ModelFramework, ModelType
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestFaceDlibLockRelease(unittest.TestCase):
|
|
10
|
-
"""H4: Lock must be released even if face_recognition raises."""
|
|
11
|
-
|
|
12
|
-
def test_lock_released_on_exception(self):
|
|
13
|
-
# Inject mock modules for the lazy imports inside detect()
|
|
14
|
-
mock_cv2 = MagicMock()
|
|
15
|
-
mock_fr = MagicMock()
|
|
16
|
-
mock_imutils = MagicMock()
|
|
17
|
-
|
|
18
|
-
sys.modules.setdefault("cv2", mock_cv2)
|
|
19
|
-
sys.modules.setdefault("imutils", mock_imutils)
|
|
20
|
-
sys.modules.setdefault("face_recognition", mock_fr)
|
|
21
|
-
|
|
22
|
-
from pyzm.ml.backends.face_dlib import FaceDlibBackend
|
|
23
|
-
|
|
24
|
-
config = ModelConfig(
|
|
25
|
-
type=ModelType.FACE,
|
|
26
|
-
framework=ModelFramework.FACE_DLIB,
|
|
27
|
-
known_faces_dir="/tmp/known",
|
|
28
|
-
)
|
|
29
|
-
backend = FaceDlibBackend(config)
|
|
30
|
-
backend._knn = MagicMock() # pretend loaded
|
|
31
|
-
backend._auto_lock = True
|
|
32
|
-
backend.acquire_lock = MagicMock()
|
|
33
|
-
backend.release_lock = MagicMock()
|
|
34
|
-
|
|
35
|
-
mock_fr.face_locations.side_effect = RuntimeError("GPU OOM")
|
|
36
|
-
|
|
37
|
-
dummy = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
38
|
-
with self.assertRaises(RuntimeError):
|
|
39
|
-
backend.detect(dummy)
|
|
40
|
-
|
|
41
|
-
backend.acquire_lock.assert_called_once()
|
|
42
|
-
backend.release_lock.assert_called_once()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|