From 1f86fb9ed71d931111a550324b71aaacab242234 Mon Sep 17 00:00:00 2001 From: Soos3D <99700157+soos3d@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:42:14 -0400 Subject: [PATCH] feat: Phase 1 repackage to src/spectrax installable layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make SpectraX a proper package without behavior changes: pyproject.toml at repo root, package rename videofeed→spectrax under src/, path helpers for config/models/TLS, lifespan instead of on_event, core vs [cv] extras so CI stays torch-free, and compiled lockfiles. Keep keychain service video-feed-mediamtx and surveillance console alias for compatibility. --- .github/workflows/ci.yml | 26 +- .gitignore | 2 +- CLAUDE.md | 106 ++++++++ README.md | 6 +- .../surveillance.yml => config/spectrax.yml | 0 {video-feed/ui => dashboard}/README.md | 4 +- {video-feed/ui => dashboard}/dashboard.html | 0 docs/API.md | 2 +- docs/ARCHITECTURE.md | 10 +- docs/PLAN.md | 33 +-- {video-feed/models => models}/README.md | 4 +- pyproject.toml | 110 +++++++++ ...quirements-dev.txt => requirements-dev.txt | 0 requirements-web-test.lock.txt | 128 ++++++++++ ...-web-test.txt => requirements-web-test.txt | 3 +- requirements.lock.txt | 232 ++++++++++++++++++ .../requirements.txt => requirements.txt | 7 +- ruff.toml | 29 --- scripts/surveillance.sh | 26 +- src/spectrax/__init__.py | 3 + {video-feed/videofeed => src/spectrax}/api.py | 0 .../videofeed => src/spectrax}/auth_gate.py | 0 .../videofeed => src/spectrax}/config.py | 0 .../videofeed => src/spectrax}/constants.py | 6 +- .../videofeed => src/spectrax}/credentials.py | 0 .../videofeed => src/spectrax}/detector.py | 6 +- .../spectrax}/detector_config.py | 0 src/spectrax/paths.py | 61 +++++ .../videofeed => src/spectrax}/recorder.py | 0 .../spectrax}/routes/README.md | 8 +- .../spectrax}/routes/__init__.py | 0 .../videofeed => src/spectrax}/routes/auth.py | 4 +- .../spectrax}/routes/files.py | 4 +- .../spectrax}/routes/pages.py | 0 .../spectrax}/routes/recordings.py | 4 +- .../spectrax}/routes/statistics.py | 4 +- .../spectrax}/routes/video.py | 2 +- .../spectrax}/surveillance.py | 41 ++-- .../spectrax}/templates/login.html | 0 .../spectrax}/templates/recordings.html | 0 .../spectrax}/templates/viewer.html | 0 .../videofeed => src/spectrax}/utils.py | 4 +- .../videofeed => src/spectrax}/visualizer.py | 49 ++-- {video-feed/tests => tests}/README.md | 6 +- {video-feed/tests => tests}/__init__.py | 0 {video-feed/tests => tests}/conftest.py | 36 ++- .../test_api_characterization.py | 4 +- {video-feed/tests => tests}/test_auth.py | 6 +- .../tests => tests}/test_config_security.py | 6 +- {video-feed/tests => tests}/test_db.py | 0 .../tests => tests}/test_db_connection.py | 8 +- tests/test_package_layout.py | 83 +++++++ {video-feed/tests => tests}/test_recording.py | 4 +- .../tests => tests}/test_storage_location.py | 10 +- .../test_supervision_integration.py | 6 +- video-feed/pytest.ini | 41 ---- video-feed/setup.py | 31 --- video-feed/videofeed/__init__.py | 3 - video-feed/videofeed/cli.py | 31 --- 59 files changed, 891 insertions(+), 308 deletions(-) create mode 100644 CLAUDE.md rename video-feed/config/surveillance.yml => config/spectrax.yml (100%) rename {video-feed/ui => dashboard}/README.md (93%) rename {video-feed/ui => dashboard}/dashboard.html (100%) rename {video-feed/models => models}/README.md (93%) create mode 100644 pyproject.toml rename video-feed/requirements-dev.txt => requirements-dev.txt (100%) create mode 100644 requirements-web-test.lock.txt rename video-feed/requirements-web-test.txt => requirements-web-test.txt (72%) create mode 100644 requirements.lock.txt rename video-feed/requirements.txt => requirements.txt (93%) delete mode 100644 ruff.toml create mode 100644 src/spectrax/__init__.py rename {video-feed/videofeed => src/spectrax}/api.py (100%) rename {video-feed/videofeed => src/spectrax}/auth_gate.py (100%) rename {video-feed/videofeed => src/spectrax}/config.py (100%) rename {video-feed/videofeed => src/spectrax}/constants.py (62%) rename {video-feed/videofeed => src/spectrax}/credentials.py (100%) rename {video-feed/videofeed => src/spectrax}/detector.py (99%) rename {video-feed/videofeed => src/spectrax}/detector_config.py (100%) create mode 100644 src/spectrax/paths.py rename {video-feed/videofeed => src/spectrax}/recorder.py (100%) rename {video-feed/videofeed => src/spectrax}/routes/README.md (94%) rename {video-feed/videofeed => src/spectrax}/routes/__init__.py (100%) rename {video-feed/videofeed => src/spectrax}/routes/auth.py (95%) rename {video-feed/videofeed => src/spectrax}/routes/files.py (97%) rename {video-feed/videofeed => src/spectrax}/routes/pages.py (100%) rename {video-feed/videofeed => src/spectrax}/routes/recordings.py (98%) rename {video-feed/videofeed => src/spectrax}/routes/statistics.py (98%) rename {video-feed/videofeed => src/spectrax}/routes/video.py (97%) rename {video-feed/videofeed => src/spectrax}/surveillance.py (95%) rename {video-feed/videofeed => src/spectrax}/templates/login.html (100%) rename {video-feed/videofeed => src/spectrax}/templates/recordings.html (100%) rename {video-feed/videofeed => src/spectrax}/templates/viewer.html (100%) rename {video-feed/videofeed => src/spectrax}/utils.py (97%) rename {video-feed/videofeed => src/spectrax}/visualizer.py (93%) rename {video-feed/tests => tests}/README.md (96%) rename {video-feed/tests => tests}/__init__.py (100%) rename {video-feed/tests => tests}/conftest.py (87%) rename {video-feed/tests => tests}/test_api_characterization.py (97%) rename {video-feed/tests => tests}/test_auth.py (97%) rename {video-feed/tests => tests}/test_config_security.py (90%) rename {video-feed/tests => tests}/test_db.py (100%) rename {video-feed/tests => tests}/test_db_connection.py (84%) create mode 100644 tests/test_package_layout.py rename {video-feed/tests => tests}/test_recording.py (97%) rename {video-feed/tests => tests}/test_storage_location.py (87%) rename {video-feed/tests => tests}/test_supervision_integration.py (94%) delete mode 100644 video-feed/pytest.ini delete mode 100644 video-feed/setup.py delete mode 100644 video-feed/videofeed/__init__.py delete mode 100644 video-feed/videofeed/cli.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a15d1a..4a3b2f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,7 @@ jobs: fail-fast: false matrix: python-version: ["3.11", "3.12"] - # macOS matrix deferred until Phase 0 bootstrap is stable (see docs/PLAN.md) - - defaults: - run: - working-directory: video-feed + # macOS matrix deferred (see docs/PLAN.md) steps: - uses: actions/checkout@v4 @@ -27,30 +23,30 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install web-test + dev dependencies + - name: Install package (web-test + dev extras) run: | python -m pip install --upgrade pip - pip install -r requirements-web-test.txt -r requirements-dev.txt + pip install -e ".[web-test,dev]" - - name: Ruff (Phase 0 paths — correctness rules) + - name: Ruff (Phase 1 paths — correctness rules) run: | # Full style cleanup deferred (repo predates ruff); gate on bugs only ruff check --select E,F,B \ - videofeed/auth_gate.py \ - videofeed/credentials.py \ - videofeed/routes/auth.py \ + src/spectrax/auth_gate.py \ + src/spectrax/credentials.py \ + src/spectrax/paths.py \ + src/spectrax/routes/auth.py \ tests/test_api_characterization.py \ tests/test_auth.py \ tests/test_config_security.py \ + tests/test_package_layout.py \ tests/conftest.py - name: Pytest (API / unit, no torch, no MediaMTX) - env: - PYTHONPATH: . run: | - # Only Phase 0 API tests: test_db*.py import recorder → cv2 (full stack) pytest \ tests/test_api_characterization.py \ tests/test_auth.py \ tests/test_config_security.py \ - -m "not slow and not requires_mediamtx" \ No newline at end of file + tests/test_package_layout.py \ + -m "not slow and not requires_mediamtx" diff --git a/.gitignore b/.gitignore index 074f134..dd08a3f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ server.key mediamtx.yml # YOLO Models (large files) -video-feed/models/*.pt +models/*.pt *.pt # Distribution / packaging .Python diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b6d7b21 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Layout + +The installable package lives under **`src/spectrax/`** (import name `spectrax`). +Install from the repo root with an editable install — do **not** set `PYTHONPATH`. + +``` +src/spectrax/ # Python package +config/spectrax.yml # example config (no secrets) +models/ # YOLO weights (gitignored *.pt) +tests/ # pytest suite +dashboard/ # orphaned static HTML until Phase 4 +``` + +## Environment + +A `venv/` (or `.venv/`) at the repo root is the expected environment; activate it +before running anything. `scripts/surveillance.sh` auto-activates it. + +```bash +python3 -m venv venv && source venv/bin/activate +pip install -e ".[dev]" # full stack (torch, etc.) +# or, for API tests only (no torch): +pip install -e ".[web-test,dev]" +``` + +MediaMTX must be on `PATH` (`brew install mediamtx`); the launcher hard-fails without it. +There are no env vars to configure — all config lives in `config/spectrax.yml`. +Credentials are generated at runtime into the OS keychain (service +`video-feed-mediamtx` — name kept for Phase 0 compatibility), never into files; +`spectrax reset` wipes them. + +## Running + +From the repo root after editable install: + +```bash +./scripts/surveillance.sh config # start from config/spectrax.yml +./scripts/surveillance.sh quick # one camera, defaults +spectrax config # same as above via console script +surveillance config # temporary alias (kept until Phase 2) +``` + +The CLI is Typer (`src/spectrax/surveillance.py`): `config`, `start`, `quick`, +`run`, `detect`, `reset`, `admin`, `apikey`, `credentials`. + +## Testing + +Run pytest from the **repo root** (not a nested package dir): + +```bash +pytest +pytest tests/test_auth.py::test_name # single test +pytest -m unit # by marker +``` + +`pyproject.toml` sets `--strict-markers`, so any new marker must be added to +`[tool.pytest.ini_options].markers` +(`unit, integration, db, recording, detection, slow, requires_mediamtx, api`). +CI uses the slim web-test extra (no torch) and runs characterization/auth/layout tests only. + +Coverage is sparse and concentrated on DB/recording/storage/auth; many modules +still lack tests. Characterization tests in `tests/test_api_characterization.py` +are the regression net for routes. + +## Style + +Ruff config lives in `pyproject.toml` (`ruff check .`, `ruff format .`). +The existing code predates it and is not clean — lint the files you touch; +do not mass-reformat the repo as a side effect of another change. + +Otherwise follow the conventions of the file you are editing: 4-space indent, +docstrings on public modules and functions, `typing` annotations, `@dataclass` +for config objects, relative or absolute package imports (`from spectrax…` +or `from .constants import …`). + +## Git + +Branch off `main` with a type prefix (`docs/`, `feat/`, `fix/`) and merge via PR. +Never commit directly to `main`. + +## Gotchas + +- **No `sys.path` / `PYTHONPATH` hacks** — use `pip install -e .`. +- **Routes use module-level global state, not DI.** `visualizer.py` wires them at + startup via setters (`video_routes.set_detector_manager`, + `files_routes.set_recordings_directory`, + `recordings_routes.set_recordings_api`). Miss one and the route 500s. See + `src/spectrax/routes/README.md`. New endpoints belong in `routes/`, not + `visualizer.py`. (DI is Phase 2.) +- **Two class filters that look alike**: `detection.filters.classes` controls + what is *detected*, `recording.record_objects` controls what is *recorded*. + Empty list means "all" for both. +- **`recording.codec` must be `avc1`** — `mp4v` produces clips the browser + player cannot play. Re-verify playback after any OpenCV bump. +- **RTSPS uses a self-signed cert** generated at runtime (`server.crt` / + `server.key` / `mediamtx.yml` are gitignored); clients will show security + warnings. API auth is session cookie or bearer API key (Phase 0). +- **Path helpers** for config/models/TLS live in `spectrax.paths` — do not + reintroduce `Path(__file__).parent.parent / "config"`. +- **Docs are known-stale — trust the code.** Full rewrite is Phase 4. + `docs/ARCHITECTURE.md` and some README paths still mention the old + `video-feed/` layout. diff --git a/README.md b/README.md index 46d7771..cd69449 100644 --- a/README.md +++ b/README.md @@ -165,16 +165,16 @@ Access the web interface at the URL shown when starting the system (e.g., `http: ./scripts/surveillance.sh quick # Start streaming server only (no detection) -python -m videofeed.surveillance run --path video/front-door +python -m spectrax.surveillance run --path video/front-door # Start detection only (existing stream) -python -m videofeed.surveillance detect --rtsp-url "rtsps://viewer:pass@host:8322/video/cam" +python -m spectrax.surveillance detect --rtsp-url "rtsps://viewer:pass@host:8322/video/cam" # Query recordings by tracker ID python scripts/query_recordings.py tracker 42 # Reset stored credentials -python -m videofeed.surveillance reset +python -m spectrax.surveillance reset ``` ### REST API diff --git a/video-feed/config/surveillance.yml b/config/spectrax.yml similarity index 100% rename from video-feed/config/surveillance.yml rename to config/spectrax.yml diff --git a/video-feed/ui/README.md b/dashboard/README.md similarity index 93% rename from video-feed/ui/README.md rename to dashboard/README.md index 6cf2157..2a309bb 100644 --- a/video-feed/ui/README.md +++ b/dashboard/README.md @@ -2,7 +2,7 @@ This directory contains a standalone HTML dashboard for quick access to surveillance feeds. -## Status (Phase 0) +## Status (Phase 0/1) **Unsupported for production use.** The unauthenticated `/paths` discovery server (`localhost:3333`) was removed in Phase 0. Opening `dashboard.html` via `file://` or @@ -11,7 +11,7 @@ cross-origin auto-discovery no longer works. Use the **integrated dashboard** served by the FastAPI app (same origin, session cookie): ```bash -# After: surveillance admin set-password +# After: spectrax admin set-password ./scripts/surveillance.sh config # Open http://127.0.0.1:8080/login ``` diff --git a/video-feed/ui/dashboard.html b/dashboard/dashboard.html similarity index 100% rename from video-feed/ui/dashboard.html rename to dashboard/dashboard.html diff --git a/docs/API.md b/docs/API.md index cdccdbf..db6e012 100644 --- a/docs/API.md +++ b/docs/API.md @@ -544,7 +544,7 @@ Currently, there are no rate limits on API endpoints. For production deployments ## CORS -Cross-Origin Resource Sharing (CORS) is enabled by default for all origins. To restrict access, modify the FastAPI CORS middleware configuration in `videofeed/visualizer.py`. +Cross-Origin Resource Sharing (CORS) is enabled by default for all origins. To restrict access, modify the FastAPI CORS middleware configuration in `spectrax/visualizer.py`. --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 19a98ff..45ee8b2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -19,7 +19,7 @@ This document provides a comprehensive overview of the SpectraX codebase archite ``` root/ ├── video-feed/ # 📦 Main Python package -│ ├── videofeed/ # Core modules +│ ├── spectrax/ # Core modules │ │ ├── surveillance.py # 🎯 MAIN ENTRY POINT - Unified CLI │ │ ├── config.py # 🔧 Configuration management │ │ ├── detector.py # 🎯 YOLO object detection @@ -105,7 +105,7 @@ class SurveillanceConfig: **Usage:** ```python -from videofeed.config import SurveillanceConfig +from spectrax.config import SurveillanceConfig config = SurveillanceConfig.load_from_yaml('config/surveillance.yml') cameras = config.get_camera_paths() @@ -588,7 +588,7 @@ pytest tests/ ```bash # Start with config file -python -m videofeed.surveillance config +python -m spectrax.surveillance config # Or use the shell script ./scripts/surveillance.sh config @@ -621,7 +621,7 @@ pytest pytest tests/test_detector.py # Run with coverage -pytest --cov=videofeed tests/ +pytest --cov=spectrax tests/ # Run with verbose output pytest -v @@ -631,7 +631,7 @@ pytest -v ```python import pytest -from videofeed.detector import ObjectDetector +from spectrax.detector import ObjectDetector def test_detector_initialization(): detector = ObjectDetector(model_path="yolov8n.pt", confidence=0.4) diff --git a/docs/PLAN.md b/docs/PLAN.md index 2652361..1d6736c 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -1,7 +1,7 @@ # SpectraX Modernization & Re-architecture Plan -> Status: **Phase 0 implemented on `feat/phase-0-security`** · Written 2026-08-09 · -> Phase 0 detail confirmed 2026-08-10 · Supersedes nothing (first plan doc) +> Status: **Phase 0 on `main`** · **Phase 1 on `feat/phase-1-repackage` (merge pending)** · +> Written 2026-08-09 · Phase 0 confirmed 2026-08-10 · Phase 1 implemented 2026-08-10 > > Sources: full-codebase survey, security audit, and architecture design produced 2026-08-09; > Phase 0 PR DAG refined by `plan-next-phase` workflow 2026-08-10. @@ -201,7 +201,7 @@ spectrax/ ### Phase 0 — Stop the bleeding (security, on the CURRENT layout) — 4–7 days -**Status: implemented on branch `feat/phase-0-security` (merge pending).** +**Status: merged to `main` (PR #9).** Fixes both CRITICALs and the HIGHs before any restructuring, so security never waits on architecture. Work stays on `video-feed/videofeed/` — **no** `create_app`, `src/spectrax/`, @@ -252,13 +252,18 @@ Checklist (maps to original items): ### Phase 1 — Repackage — days Mechanical only, no logic changes. -1. `pyproject.toml` (setuptools backend, `requires-python >= 3.11`, both console scripts); - delete `setup.py`, `cli.py` shim, and the `sys.path` hack. -2. `git mv` to `src/spectrax/` layout; fix imports; ruff config moves into pyproject. -3. Dependency refresh in tiers (web stack → CV stack → utilities), tests between tiers; - compiled lockfile (`uv pip compile`). **Re-verify `avc1` clip playback in-browser after - the OpenCV bump.** -4. `@app.on_event` → lifespan context; clear pydantic v2 deprecation warnings. +**Status: implemented on branch `feat/phase-1-repackage` (merge pending).** + +1. `pyproject.toml` (setuptools backend, `requires-python >= 3.11`, console scripts + `spectrax` + `surveillance` alias); delete `setup.py`, `cli.py` shim, and the + `sys.path` hack. Core deps exclude torch; full stack is `pip install -e ".[cv]"`. — **done** +2. `git mv` to `src/spectrax/` layout; package rename `videofeed` → `spectrax`; path + helpers in `spectrax.paths`; ruff/pytest config in pyproject; config at + `config/spectrax.yml`; keychain service string kept as `video-feed-mediamtx`. — **done** +3. Compiled lockfiles via `uv pip compile` (`requirements.lock.txt`, + `requirements-web-test.lock.txt`). Direct pin bumps (OpenCV/`avc1` gate) deferred to a + follow-up so this PR stays mechanical. — **done (structure); version bumps deferred** +4. `@app.on_event` → lifespan context. — **done** *Ships: identical behavior, proper installable package, current deps.* @@ -342,10 +347,10 @@ URLs, Phase 0 tests as the regression net. 1. ~~**Dashboard TLS**~~ — **Decided (Phase 0):** plain HTTP on trusted LAN; `Secure=False` by default; `HttpOnly` + `SameSite=Strict` always. Self-signed dashboard HTTPS deferred. -2. **Rename**: package becomes `spectrax` (from `videofeed`) in Phase 1 — confirm the name - before the `git mv`. -3. **`.enc` extension** in the file-serving allowlist looks like an abandoned encryption - feature — confirm dead and remove in Phase 1. +2. ~~**Rename**~~ — **Decided (Phase 1):** package is `spectrax`; console scripts + `spectrax` + temporary `surveillance` alias. Keychain service stays + `video-feed-mediamtx`. +3. ~~**`.enc` extension**~~ — **Decided (Phase 1):** removed from files allowlist (dead). 4. **MediaMTX ownership**: keep spawning it as a child of the core (simplest, current behavior) vs separate systemd unit on Linux (survives core restarts)? Plan assumes child-process in dev, separate unit in production — confirm in Phase 2. diff --git a/video-feed/models/README.md b/models/README.md similarity index 93% rename from video-feed/models/README.md rename to models/README.md index df2de0e..a059c22 100644 --- a/video-feed/models/README.md +++ b/models/README.md @@ -20,7 +20,7 @@ The system automatically uses models from this directory when you specify a mode Models are automatically loaded from this directory when you specify them in: -1. **Configuration file** (`video-feed/config/surveillance.yml`): +1. **Configuration file** (`config/spectrax.yml`): ```yaml detection: model: "yolov8l.pt" @@ -28,7 +28,7 @@ Models are automatically loaded from this directory when you specify them in: 2. **Command line**: ```bash - python -m videofeed.surveillance start --model yolov8n.pt + spectrax start --model yolov8n.pt ``` ## Automatic Download diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a25e93b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,110 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "spectrax" +version = "0.2.0" +description = "Local surveillance core: MediaMTX streaming, YOLO detection, recording, and dashboard API" +readme = "README.md" +requires-python = ">=3.11" +license = { text = "MIT" } +authors = [{ name = "SpectraCoreX" }] +# Core = CLI + API + auth (no torch). Detection/recording runtime needs the [cv] extra. +dependencies = [ + "typer==0.15.3", + "click==8.1.8", # pin: click 8.2+ breaks typer 0.15 help (make_metavar) + "PyYAML==6.0.2", + "keyring==25.6.0", + "fastapi==0.115.12", + "uvicorn==0.34.2", + "Jinja2==3.1.6", + "starlette==0.46.2", + "itsdangerous==2.2.0", + "argon2-cffi==23.1.0", + "python-multipart==0.0.20", + "pydantic==2.11.4", + "Pillow==11.2.1", + "requests==2.32.3", + "tqdm==4.67.1", + "psutil==7.0.0", +] + +[project.optional-dependencies] +# Computer vision / detection stack (full runtime) +cv = [ + "opencv-python-headless==4.11.0.86", + "ultralytics==8.3.129", + "supervision==0.26.1", + "torch==2.7.0", + "torchvision==0.22.0", + "numpy==2.2.5", +] +# Alias used by CI docs: core + test tools, no torch +web-test = [] +dev = [ + "pytest==8.3.5", + "pytest-cov==6.1.1", + "pytest-asyncio==0.26.0", + "httpx==0.28.1", + "ruff==0.11.6", +] + +[project.scripts] +spectrax = "spectrax.surveillance:app" +surveillance = "spectrax.surveillance:app" + +[tool.setuptools.package-dir] +"" = "src" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +spectrax = ["templates/*.html"] + +[tool.ruff] +target-version = "py311" +line-length = 100 +extend-exclude = ["venv", ".venv", "models"] + +[tool.ruff.lint] +select = [ + "E", + "F", + "W", + "I", + "UP", + "B", + "C4", + "SIM", +] +ignore = [ + "E501", + "B008", +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["F401", "F811"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = ["-v", "--strict-markers", "--tb=short", "--disable-warnings"] +asyncio_default_fixture_loop_scope = "function" +markers = [ + "unit: Unit tests for individual components", + "integration: Integration tests for component interaction", + "db: Database-related tests", + "recording: Recording functionality tests", + "detection: Object detection tests", + "slow: Tests that take significant time to run", + "requires_mediamtx: Tests that require MediaMTX to be installed", + "api: API/route characterization and auth tests (no torch/MediaMTX)", +] diff --git a/video-feed/requirements-dev.txt b/requirements-dev.txt similarity index 100% rename from video-feed/requirements-dev.txt rename to requirements-dev.txt diff --git a/requirements-web-test.lock.txt b/requirements-web-test.lock.txt new file mode 100644 index 0000000..9872b42 --- /dev/null +++ b/requirements-web-test.lock.txt @@ -0,0 +1,128 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --extra web-test --extra dev -o requirements-web-test.lock.txt +annotated-types==0.8.0 + # via pydantic +anyio==4.14.2 + # via + # httpx + # starlette +argon2-cffi==23.1.0 + # via spectrax (pyproject.toml) +argon2-cffi-bindings==25.1.0 + # via argon2-cffi +certifi==2026.7.22 + # via + # httpcore + # httpx + # requests +cffi==2.1.1 + # via argon2-cffi-bindings +charset-normalizer==3.4.9 + # via requests +click==8.1.8 + # via + # spectrax (pyproject.toml) + # typer + # uvicorn +coverage==7.15.4 + # via pytest-cov +fastapi==0.115.12 + # via spectrax (pyproject.toml) +h11==0.16.0 + # via + # httpcore + # uvicorn +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via spectrax (pyproject.toml) +idna==3.18 + # via + # anyio + # httpx + # requests +iniconfig==2.3.0 + # via pytest +itsdangerous==2.2.0 + # via spectrax (pyproject.toml) +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.1.2 + # via keyring +jaraco-functools==4.6.0 + # via keyring +jinja2==3.1.6 + # via spectrax (pyproject.toml) +keyring==25.6.0 + # via spectrax (pyproject.toml) +markdown-it-py==4.2.0 + # via rich +markupsafe==3.0.3 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +more-itertools==11.1.0 + # via + # jaraco-classes + # jaraco-functools +packaging==26.3 + # via pytest +pillow==11.2.1 + # via spectrax (pyproject.toml) +pluggy==1.6.0 + # via pytest +psutil==7.0.0 + # via spectrax (pyproject.toml) +pycparser==3.0 + # via cffi +pydantic==2.11.4 + # via + # spectrax (pyproject.toml) + # fastapi +pydantic-core==2.33.2 + # via pydantic +pygments==2.20.0 + # via rich +pytest==8.3.5 + # via + # spectrax (pyproject.toml) + # pytest-asyncio + # pytest-cov +pytest-asyncio==0.26.0 + # via spectrax (pyproject.toml) +pytest-cov==6.1.1 + # via spectrax (pyproject.toml) +python-multipart==0.0.20 + # via spectrax (pyproject.toml) +pyyaml==6.0.2 + # via spectrax (pyproject.toml) +requests==2.32.3 + # via spectrax (pyproject.toml) +rich==15.0.0 + # via typer +ruff==0.11.6 + # via spectrax (pyproject.toml) +shellingham==1.5.4 + # via typer +starlette==0.46.2 + # via + # spectrax (pyproject.toml) + # fastapi +tqdm==4.67.1 + # via spectrax (pyproject.toml) +typer==0.15.3 + # via spectrax (pyproject.toml) +typing-extensions==4.16.0 + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # typer + # typing-inspection +typing-inspection==0.4.3 + # via pydantic +urllib3==2.7.0 + # via requests +uvicorn==0.34.2 + # via spectrax (pyproject.toml) diff --git a/video-feed/requirements-web-test.txt b/requirements-web-test.txt similarity index 72% rename from video-feed/requirements-web-test.txt rename to requirements-web-test.txt index f1bb1b0..d0c5da2 100644 --- a/video-feed/requirements-web-test.txt +++ b/requirements-web-test.txt @@ -1,5 +1,6 @@ # Slim web stack for API/route tests (no torch / ultralytics / opencv). -# Full runtime install remains requirements.txt. +# Prefer: pip install -e ".[web-test,dev]" +# Full runtime: pip install -e ".[cv]" or requirements.txt. typer==0.15.3 PyYAML==6.0.2 diff --git a/requirements.lock.txt b/requirements.lock.txt new file mode 100644 index 0000000..6a7b2bd --- /dev/null +++ b/requirements.lock.txt @@ -0,0 +1,232 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --extra cv --extra dev -o requirements.lock.txt +annotated-types==0.8.0 + # via pydantic +anyio==4.14.2 + # via + # httpx + # starlette +argon2-cffi==23.1.0 + # via spectrax (pyproject.toml) +argon2-cffi-bindings==25.1.0 + # via argon2-cffi +certifi==2026.7.22 + # via + # httpcore + # httpx + # requests +cffi==2.1.1 + # via argon2-cffi-bindings +charset-normalizer==3.4.9 + # via requests +click==8.1.8 + # via + # spectrax (pyproject.toml) + # typer + # uvicorn +contourpy==1.3.3 + # via matplotlib +coverage==7.15.4 + # via pytest-cov +cycler==0.12.1 + # via matplotlib +defusedxml==0.7.1 + # via supervision +fastapi==0.115.12 + # via spectrax (pyproject.toml) +filelock==3.32.2 + # via torch +fonttools==4.63.0 + # via matplotlib +fsspec==2026.7.0 + # via torch +h11==0.16.0 + # via + # httpcore + # uvicorn +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via spectrax (pyproject.toml) +idna==3.18 + # via + # anyio + # httpx + # requests +iniconfig==2.3.0 + # via pytest +itsdangerous==2.2.0 + # via spectrax (pyproject.toml) +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.1.2 + # via keyring +jaraco-functools==4.6.0 + # via keyring +jinja2==3.1.6 + # via + # spectrax (pyproject.toml) + # torch +keyring==25.6.0 + # via spectrax (pyproject.toml) +kiwisolver==1.5.0 + # via matplotlib +markdown-it-py==4.2.0 + # via rich +markupsafe==3.0.3 + # via jinja2 +matplotlib==3.11.1 + # via + # seaborn + # supervision + # ultralytics +mdurl==0.1.2 + # via markdown-it-py +more-itertools==11.1.0 + # via + # jaraco-classes + # jaraco-functools +mpmath==1.3.0 + # via sympy +networkx==3.6.1 + # via torch +numpy==2.2.5 + # via + # spectrax (pyproject.toml) + # contourpy + # matplotlib + # opencv-python + # opencv-python-headless + # pandas + # scipy + # seaborn + # supervision + # torchvision + # ultralytics + # ultralytics-thop +opencv-python==5.0.0.93 + # via + # supervision + # ultralytics +opencv-python-headless==4.11.0.86 + # via spectrax (pyproject.toml) +packaging==26.3 + # via + # matplotlib + # pytest +pandas==3.0.5 + # via + # seaborn + # ultralytics +pillow==11.2.1 + # via + # spectrax (pyproject.toml) + # matplotlib + # supervision + # torchvision + # ultralytics +pluggy==1.6.0 + # via pytest +psutil==7.0.0 + # via + # spectrax (pyproject.toml) + # ultralytics +py-cpuinfo==9.0.0 + # via ultralytics +pycparser==3.0 + # via cffi +pydantic==2.11.4 + # via + # spectrax (pyproject.toml) + # fastapi +pydantic-core==2.33.2 + # via pydantic +pygments==2.20.0 + # via rich +pyparsing==3.3.2 + # via matplotlib +pytest==8.3.5 + # via + # spectrax (pyproject.toml) + # pytest-asyncio + # pytest-cov +pytest-asyncio==0.26.0 + # via spectrax (pyproject.toml) +pytest-cov==6.1.1 + # via spectrax (pyproject.toml) +python-dateutil==2.9.0.post0 + # via + # matplotlib + # pandas +python-multipart==0.0.20 + # via spectrax (pyproject.toml) +pyyaml==6.0.2 + # via + # spectrax (pyproject.toml) + # supervision + # ultralytics +requests==2.32.3 + # via + # spectrax (pyproject.toml) + # supervision + # ultralytics +rich==15.0.0 + # via typer +ruff==0.11.6 + # via spectrax (pyproject.toml) +scipy==1.18.0 + # via + # supervision + # ultralytics +seaborn==0.13.2 + # via ultralytics +setuptools==84.0.0 + # via torch +shellingham==1.5.4 + # via typer +six==1.17.0 + # via python-dateutil +starlette==0.46.2 + # via + # spectrax (pyproject.toml) + # fastapi +supervision==0.26.1 + # via spectrax (pyproject.toml) +sympy==1.14.0 + # via torch +torch==2.7.0 + # via + # spectrax (pyproject.toml) + # torchvision + # ultralytics + # ultralytics-thop +torchvision==0.22.0 + # via + # spectrax (pyproject.toml) + # ultralytics +tqdm==4.67.1 + # via + # spectrax (pyproject.toml) + # supervision + # ultralytics +typer==0.15.3 + # via spectrax (pyproject.toml) +typing-extensions==4.16.0 + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # torch + # typer + # typing-inspection +typing-inspection==0.4.3 + # via pydantic +ultralytics==8.3.129 + # via spectrax (pyproject.toml) +ultralytics-thop==2.1.6 + # via ultralytics +urllib3==2.7.0 + # via requests +uvicorn==0.34.2 + # via spectrax (pyproject.toml) diff --git a/video-feed/requirements.txt b/requirements.txt similarity index 93% rename from video-feed/requirements.txt rename to requirements.txt index 0ff48bc..ce396dc 100644 --- a/video-feed/requirements.txt +++ b/requirements.txt @@ -1,5 +1,8 @@ -# SpectraX Surveillance System - Core Dependencies -# Optimized requirements file (cleaned up 2025-10-02) +# SpectraX full runtime pin set (core + CV). +# Preferred install from repo root: +# pip install -e ".[cv,dev]" # full stack +# pip install -e ".[web-test,dev]" # API tests only (no torch) +# This file remains a flat pin dump for operators who do not use extras. # ============================================================ # Core Framework & CLI diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index a4f377c..0000000 --- a/ruff.toml +++ /dev/null @@ -1,29 +0,0 @@ -# Ruff configuration for SpectraX. -# Run from the repo root: `ruff check .` and `ruff format .` - -target-version = "py38" -line-length = 100 -extend-exclude = ["venv", ".venv", "video-feed/models"] - -[lint] -select = [ - "E", # pycodestyle errors - "F", # pyflakes - "W", # pycodestyle warnings - "I", # isort (import ordering) - "UP", # pyupgrade - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "SIM", # flake8-simplify -] -ignore = [ - "E501", # line length is enforced by the formatter, not the linter - "B008", # FastAPI/Typer rely on function calls in argument defaults -] - -[lint.per-file-ignores] -"video-feed/tests/*" = ["F401", "F811"] # pytest fixtures look like unused/shadowed imports - -[format] -quote-style = "double" -indent-style = "space" diff --git a/scripts/surveillance.sh b/scripts/surveillance.sh index 424e88a..c41aeee 100755 --- a/scripts/surveillance.sh +++ b/scripts/surveillance.sh @@ -26,41 +26,47 @@ elif [ -d "$PROJECT_ROOT/.venv" ]; then source "$PROJECT_ROOT/.venv/bin/activate" fi -# Set Python path to include the video-feed directory -export PYTHONPATH="$PROJECT_ROOT/video-feed:$PYTHONPATH" +# Prefer the installed console script; fall back to python -m +if command -v spectrax &> /dev/null; then + CLI=(spectrax) +elif command -v surveillance &> /dev/null; then + CLI=(surveillance) +else + CLI=(python3 -m spectrax.surveillance) +fi # Parse command line arguments case "$1" in quick) echo "🚀 Quick start mode (1 camera, with detection)" cd "$PROJECT_ROOT" - python3 -m videofeed.surveillance quick + "${CLI[@]}" quick ;; config) echo "📋 Starting with configuration file..." cd "$PROJECT_ROOT" - python3 -m videofeed.surveillance config + "${CLI[@]}" config ;; custom) echo "⚙️ Custom mode - specify your options:" shift cd "$PROJECT_ROOT" - python3 -m videofeed.surveillance start "$@" + "${CLI[@]}" start "$@" ;; dashboard) echo "🌐 Opening surveillance dashboard..." - open "$PROJECT_ROOT/video-feed/ui/dashboard.html" + open "$PROJECT_ROOT/dashboard/dashboard.html" ;; *) - echo "Usage: ./surveillance.sh [quick|config|custom|dashboard]" + echo "Usage: ./scripts/surveillance.sh [quick|config|custom|dashboard]" echo "" echo " quick - Quick start with 1 camera and object detection" - echo " config - Start using surveillance.yml configuration" + echo " config - Start using config/spectrax.yml" echo " custom - Start with custom command line options" - echo " dashboard - Open the web dashboard" + echo " dashboard - Open the standalone web dashboard (orphaned until Phase 4)" echo "" echo "Default: Starting with configuration file..." cd "$PROJECT_ROOT" - python3 -m videofeed.surveillance config + "${CLI[@]}" config ;; esac diff --git a/src/spectrax/__init__.py b/src/spectrax/__init__.py new file mode 100644 index 0000000..07905a4 --- /dev/null +++ b/src/spectrax/__init__.py @@ -0,0 +1,3 @@ +"""SpectraX core package for RTSP/HLS streaming, detection, and recording.""" + +__version__ = "0.2.0" diff --git a/video-feed/videofeed/api.py b/src/spectrax/api.py similarity index 100% rename from video-feed/videofeed/api.py rename to src/spectrax/api.py diff --git a/video-feed/videofeed/auth_gate.py b/src/spectrax/auth_gate.py similarity index 100% rename from video-feed/videofeed/auth_gate.py rename to src/spectrax/auth_gate.py diff --git a/video-feed/videofeed/config.py b/src/spectrax/config.py similarity index 100% rename from video-feed/videofeed/config.py rename to src/spectrax/config.py diff --git a/video-feed/videofeed/constants.py b/src/spectrax/constants.py similarity index 62% rename from video-feed/videofeed/constants.py rename to src/spectrax/constants.py index 41a1966..a3f74bd 100644 --- a/video-feed/videofeed/constants.py +++ b/src/spectrax/constants.py @@ -1,6 +1,8 @@ -"""Shared constants for the videofeed package.""" +"""Shared constants for the spectrax package.""" -# Application constants +# Application constants. +# APP_NAME stays "video-feed" so KEYCHAIN_SERVICE remains "video-feed-mediamtx" +# and existing Phase 0 keychain secrets keep resolving after the package rename. APP_NAME = "video-feed" KEYCHAIN_SERVICE = f"{APP_NAME}-mediamtx" diff --git a/video-feed/videofeed/credentials.py b/src/spectrax/credentials.py similarity index 100% rename from video-feed/videofeed/credentials.py rename to src/spectrax/credentials.py diff --git a/video-feed/videofeed/detector.py b/src/spectrax/detector.py similarity index 99% rename from video-feed/videofeed/detector.py rename to src/spectrax/detector.py index 4f094d7..3f4a6a1 100644 --- a/video-feed/videofeed/detector.py +++ b/src/spectrax/detector.py @@ -14,9 +14,9 @@ from concurrent.futures import ThreadPoolExecutor import uuid -from videofeed.recorder import RecordingManager -from videofeed.detector_config import DetectorConfig -from videofeed.utils import resolve_model_path +from spectrax.recorder import RecordingManager +from spectrax.detector_config import DetectorConfig +from spectrax.utils import resolve_model_path # Configure logging logging.basicConfig(level=logging.INFO, diff --git a/video-feed/videofeed/detector_config.py b/src/spectrax/detector_config.py similarity index 100% rename from video-feed/videofeed/detector_config.py rename to src/spectrax/detector_config.py diff --git a/src/spectrax/paths.py b/src/spectrax/paths.py new file mode 100644 index 0000000..3a180f0 --- /dev/null +++ b/src/spectrax/paths.py @@ -0,0 +1,61 @@ +"""Project path resolution for package, config, models, and TLS material. + +After the ``src/spectrax`` layout, data files live at the repository root +(``config/``, ``models/``, ``server.key``), not next to the Python package. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Tuple + + +def project_root() -> Path: + """Return the repository / install project root. + + Walks up from this file looking for ``pyproject.toml`` (name spectrax when + present). Falls back to two parents above the package directory + (``src/spectrax`` → repo root in a normal checkout). + """ + here = Path(__file__).resolve().parent + for candidate in (here, *here.parents): + pyproject = candidate / "pyproject.toml" + if not pyproject.is_file(): + continue + try: + text = pyproject.read_text(encoding="utf-8") + except OSError: + return candidate + # Prefer the spectrax project file if several pyproject.toml exist. + if 'name = "spectrax"' in text or "name = 'spectrax'" in text: + return candidate + return candidate + # src/spectrax → repo root + return here.parent.parent + + +def default_config_path() -> Path: + """Path to the example/default YAML config. + + Prefers ``config/spectrax.yml``; falls back to ``config/surveillance.yml`` + for one release of compatibility after the rename. + """ + root = project_root() + preferred = root / "config" / "spectrax.yml" + if preferred.is_file(): + return preferred + legacy = root / "config" / "surveillance.yml" + if legacy.is_file(): + return legacy + return preferred + + +def default_tls_paths() -> Tuple[Path, Path]: + """Default MediaMTX TLS key/cert paths at the project root.""" + root = project_root() + return root / "server.key", root / "server.crt" + + +def models_dir() -> Path: + """Directory used for packaged / checked-in YOLO model weights.""" + return project_root() / "models" diff --git a/video-feed/videofeed/recorder.py b/src/spectrax/recorder.py similarity index 100% rename from video-feed/videofeed/recorder.py rename to src/spectrax/recorder.py diff --git a/video-feed/videofeed/routes/README.md b/src/spectrax/routes/README.md similarity index 94% rename from video-feed/videofeed/routes/README.md rename to src/spectrax/routes/README.md index 836bed6..46c6d50 100644 --- a/video-feed/videofeed/routes/README.md +++ b/src/spectrax/routes/README.md @@ -64,7 +64,7 @@ routes/ Routes are automatically included in the main FastAPI app via `visualizer.py`: ```python -from videofeed.routes import ( +from spectrax.routes import ( video_router, pages_router, files_router, @@ -87,15 +87,15 @@ Some route modules require access to global instances (detector_manager, recordi ```python # Set detector manager -import videofeed.routes.video as video_routes +import spectrax.routes.video as video_routes video_routes.set_detector_manager(detector_manager) # Set recordings directory -import videofeed.routes.files as files_routes +import spectrax.routes.files as files_routes files_routes.set_recordings_directory(recordings_directory) # Set recordings API -import videofeed.routes.recordings as recordings_routes +import spectrax.routes.recordings as recordings_routes recordings_routes.set_recordings_api(recordings_api) ``` diff --git a/video-feed/videofeed/routes/__init__.py b/src/spectrax/routes/__init__.py similarity index 100% rename from video-feed/videofeed/routes/__init__.py rename to src/spectrax/routes/__init__.py diff --git a/video-feed/videofeed/routes/auth.py b/src/spectrax/routes/auth.py similarity index 95% rename from video-feed/videofeed/routes/auth.py rename to src/spectrax/routes/auth.py index 9cd67f8..e4b4e56 100644 --- a/video-feed/videofeed/routes/auth.py +++ b/src/spectrax/routes/auth.py @@ -5,8 +5,8 @@ from fastapi import APIRouter, HTTPException, Request, Response from pydantic import BaseModel, Field -from videofeed import credentials as creds_mod -from videofeed.auth_gate import ( +from spectrax import credentials as creds_mod +from spectrax.auth_gate import ( check_login_rate_limit, clear_login_attempts, clear_session_cookie, diff --git a/video-feed/videofeed/routes/files.py b/src/spectrax/routes/files.py similarity index 97% rename from video-feed/videofeed/routes/files.py rename to src/spectrax/routes/files.py index 2f4ad4f..72e770d 100644 --- a/video-feed/videofeed/routes/files.py +++ b/src/spectrax/routes/files.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse -from videofeed.auth_gate import AuthPrincipal, require_read +from spectrax.auth_gate import AuthPrincipal, require_read router = APIRouter(prefix="/recordings", tags=["files"]) @@ -59,7 +59,7 @@ async def serve_recording_file( if not requested_path.is_file(): raise HTTPException(status_code=403, detail="Not a file") - allowed_extensions = {".mp4", ".jpg", ".jpeg", ".png", ".webm", ".enc"} + allowed_extensions = {".mp4", ".jpg", ".jpeg", ".png", ".webm"} if requested_path.suffix.lower() not in allowed_extensions: logger.warning(f"Unauthorized file type access attempt: {requested_path.suffix}") raise HTTPException(status_code=403, detail="File type not allowed") diff --git a/video-feed/videofeed/routes/pages.py b/src/spectrax/routes/pages.py similarity index 100% rename from video-feed/videofeed/routes/pages.py rename to src/spectrax/routes/pages.py diff --git a/video-feed/videofeed/routes/recordings.py b/src/spectrax/routes/recordings.py similarity index 98% rename from video-feed/videofeed/routes/recordings.py rename to src/spectrax/routes/recordings.py index 0379320..3d8ad20 100644 --- a/video-feed/videofeed/routes/recordings.py +++ b/src/spectrax/routes/recordings.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query -from videofeed.auth_gate import AuthPrincipal, require_admin, require_read +from spectrax.auth_gate import AuthPrincipal, require_admin, require_read router = APIRouter(prefix="/api/recordings", tags=["recordings"]) @@ -48,7 +48,7 @@ def initialize_recordings_api(): return True try: - from videofeed.api import RecordingsAPI + from spectrax.api import RecordingsAPI if recordings_directory: expanded_dir = os.path.expanduser(recordings_directory) diff --git a/video-feed/videofeed/routes/statistics.py b/src/spectrax/routes/statistics.py similarity index 98% rename from video-feed/videofeed/routes/statistics.py rename to src/spectrax/routes/statistics.py index 12ac6d5..652ef43 100644 --- a/video-feed/videofeed/routes/statistics.py +++ b/src/spectrax/routes/statistics.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query -from videofeed.auth_gate import AuthPrincipal, require_read +from spectrax.auth_gate import AuthPrincipal, require_read router = APIRouter(prefix="/api", tags=["statistics"]) @@ -43,7 +43,7 @@ def initialize_recordings_api(): return True try: - from videofeed.api import RecordingsAPI + from spectrax.api import RecordingsAPI import os home_db_path = os.path.expanduser("~/video-feed-recordings/recordings.db") diff --git a/video-feed/videofeed/routes/video.py b/src/spectrax/routes/video.py similarity index 97% rename from video-feed/videofeed/routes/video.py rename to src/spectrax/routes/video.py index 47101a7..72c58cd 100644 --- a/video-feed/videofeed/routes/video.py +++ b/src/spectrax/routes/video.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse -from videofeed.auth_gate import AuthPrincipal, require_read +from spectrax.auth_gate import AuthPrincipal, require_read router = APIRouter(prefix="/video", tags=["video"]) diff --git a/video-feed/videofeed/surveillance.py b/src/spectrax/surveillance.py similarity index 95% rename from video-feed/videofeed/surveillance.py rename to src/spectrax/surveillance.py index 55b4c5d..7472e63 100644 --- a/video-feed/videofeed/surveillance.py +++ b/src/spectrax/surveillance.py @@ -12,13 +12,7 @@ import typer import yaml -# Add the parent directory to sys.path to make videofeed importable -parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -if parent_dir not in sys.path: - sys.path.insert(0, parent_dir) - -# Now import from videofeed -from videofeed.credentials import ( +from spectrax.credentials import ( get_credentials, load_config_credentials, reset_creds, @@ -27,16 +21,16 @@ list_api_keys, revoke_api_key, ) -from videofeed.config import write_cfg, load_config_paths, SurveillanceConfig -from videofeed.utils import ( +from spectrax.config import write_cfg, load_config_paths, SurveillanceConfig +from spectrax.paths import default_config_path, default_tls_paths +from spectrax.utils import ( detect_host_ip, check_mediamtx_installed, launch_mediamtx, print_urls, show_stream_credentials, ) -from videofeed.visualizer import start_visualizer -from videofeed.constants import DEFAULT_PATHS +from spectrax.constants import DEFAULT_PATHS app = typer.Typer(add_completion=False) admin_app = typer.Typer(help="Admin dashboard password management") @@ -68,8 +62,7 @@ def start_streaming_server( check_mediamtx_installed("mediamtx") # Use default TLS paths if not provided - default_tls_key = Path(__file__).parent.parent / "server.key" - default_tls_cert = Path(__file__).parent.parent / "server.crt" + default_tls_key, default_tls_cert = default_tls_paths() if not tls_key and default_tls_key.exists(): tls_key = default_tls_key @@ -165,10 +158,10 @@ def run_detector(): # typer.echo(f"🎯 Starting object detection for {len(rtsp_urls)} streams...") # Import here to avoid circular imports - from videofeed.detector import DetectorManager - from videofeed.detector_config import DetectorConfig - from videofeed.visualizer import app, set_detector_manager - from videofeed.recorder import RecordingManager + from spectrax.detector import DetectorManager + from spectrax.detector_config import DetectorConfig + from spectrax.visualizer import app, set_detector_manager + from spectrax.recorder import RecordingManager import uvicorn try: @@ -192,13 +185,13 @@ def run_detector(): detector_manager = DetectorManager(recording_manager=recording_manager) # Create detector configuration from surveillance config - # This pulls all settings from config/surveillance.yml including: + # This pulls all settings from config/spectrax.yml including: # - Model, confidence, resolution # - Stream buffer and reconnect settings # - Detection filters (classes, min/max area) # - Visual appearance (box color, label style) - from videofeed.config import SurveillanceConfig - config_path = Path(__file__).parent.parent / "config" / "surveillance.yml" + from spectrax.config import SurveillanceConfig + config_path = default_config_path() surveillance_cfg = SurveillanceConfig(config_path) detector_config = DetectorConfig.from_surveillance_config(surveillance_cfg) @@ -352,7 +345,7 @@ def config( # Use default config path if not provided if config_file is None: - config_file = Path(__file__).parent.parent / "config" / "surveillance.yml" + config_file = default_config_path() # Load configuration using unified config manager config = SurveillanceConfig(config_file) @@ -500,8 +493,7 @@ def run( check_mediamtx_installed("mediamtx") # Use default TLS paths if not provided - default_tls_key = Path(__file__).parent.parent / "server.key" - default_tls_cert = Path(__file__).parent.parent / "server.crt" + default_tls_key, default_tls_cert = default_tls_paths() if not tls_key and default_tls_key.exists(): tls_key = default_tls_key @@ -710,6 +702,9 @@ def detect( typer.secho("Press Ctrl+C once to exit cleanly.", fg=typer.colors.BRIGHT_BLACK) + # Lazy import: visualizer → detector pulls torch/opencv ([cv] extra) + from spectrax.visualizer import start_visualizer + # Start the visualizer with all URLs start_visualizer( rtsp_urls=all_urls, diff --git a/video-feed/videofeed/templates/login.html b/src/spectrax/templates/login.html similarity index 100% rename from video-feed/videofeed/templates/login.html rename to src/spectrax/templates/login.html diff --git a/video-feed/videofeed/templates/recordings.html b/src/spectrax/templates/recordings.html similarity index 100% rename from video-feed/videofeed/templates/recordings.html rename to src/spectrax/templates/recordings.html diff --git a/video-feed/videofeed/templates/viewer.html b/src/spectrax/templates/viewer.html similarity index 100% rename from video-feed/videofeed/templates/viewer.html rename to src/spectrax/templates/viewer.html diff --git a/video-feed/videofeed/utils.py b/src/spectrax/utils.py similarity index 97% rename from video-feed/videofeed/utils.py rename to src/spectrax/utils.py index 1da7a5e..f40ba9a 100644 --- a/video-feed/videofeed/utils.py +++ b/src/spectrax/utils.py @@ -9,6 +9,7 @@ from typing import Dict, List, Optional from .constants import MEDIAMTX_BIN +from .paths import models_dir def resolve_model_path(model_name: str) -> str: @@ -25,8 +26,7 @@ def resolve_model_path(model_name: str) -> str: if model_path.is_absolute() or model_path.exists(): return str(model_path) - package_models_dir = Path(__file__).parent.parent / "models" - package_model_path = package_models_dir / model_name + package_model_path = models_dir() / model_name if package_model_path.exists(): return str(package_model_path) diff --git a/video-feed/videofeed/visualizer.py b/src/spectrax/visualizer.py similarity index 93% rename from video-feed/videofeed/visualizer.py rename to src/spectrax/visualizer.py index ab7797e..8f4e9b5 100644 --- a/video-feed/videofeed/visualizer.py +++ b/src/spectrax/visualizer.py @@ -1,10 +1,11 @@ -"""API server for video-feed with object detection.""" +"""API server for SpectraX with object detection.""" import logging import os import signal import threading import time +from contextlib import asynccontextmanager from typing import List, Optional from fastapi import Depends, FastAPI, HTTPException, Request @@ -12,18 +13,18 @@ from fastapi.responses import JSONResponse import uvicorn -from videofeed.detector import DetectorManager -from videofeed.recorder import RecordingManager -from videofeed.api import RecordingsAPI -from videofeed.utils import detect_host_ip -from videofeed.auth_gate import ( +from spectrax.detector import DetectorManager +from spectrax.recorder import RecordingManager +from spectrax.api import RecordingsAPI +from spectrax.utils import detect_host_ip +from spectrax.auth_gate import ( AuthMiddleware, AuthPrincipal, require_read, ) # Import route modules -from videofeed.routes import ( +from spectrax.routes import ( video_router, pages_router, files_router, @@ -31,18 +32,31 @@ statistics_router, auth_router ) -import videofeed.routes.video as video_routes -import videofeed.routes.files as files_routes -import videofeed.routes.recordings as recordings_routes -import videofeed.routes.statistics as statistics_routes +import spectrax.routes.video as video_routes +import spectrax.routes.files as files_routes +import spectrax.routes.recordings as recordings_routes +import spectrax.routes.statistics as statistics_routes # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger('video-api-server') +@asynccontextmanager +async def lifespan(app: FastAPI): + """Start/stop hook for the FastAPI process (replaces @app.on_event).""" + yield + global detector_manager + logger.info("Server is shutting down, stopping detector...") + if detector_manager: + logger.info("Stopping all detectors...") + detector_manager.stop_all() + logger.info("All detectors stopped successfully") + logger.info("Detector stopped successfully") + + # Create FastAPI app -app = FastAPI(title="Video Feed API") +app = FastAPI(title="SpectraX API", lifespan=lifespan) # Phase 0: Secure=False on plain HTTP (trusted LAN). Set True when TLS terminates here. app.state.secure_cookies = False @@ -148,17 +162,6 @@ async def get_feeds( shutdown_requested = threading.Event() -@app.on_event("shutdown") -async def shutdown_detector(): - """Shutdown the detector when FastAPI is shutting down.""" - global detector_manager - logger.info("Server is shutting down, stopping detector...") - if detector_manager: - logger.info("Stopping all detectors...") - detector_manager.stop_all() - logger.info("All detectors stopped successfully") - logger.info("Detector stopped successfully") - def force_exit(): """Force exit after a timeout.""" time.sleep(3) # Give a few seconds for graceful shutdown diff --git a/video-feed/tests/README.md b/tests/README.md similarity index 96% rename from video-feed/tests/README.md rename to tests/README.md index a5a4d5b..b8d26cd 100644 --- a/video-feed/tests/README.md +++ b/tests/README.md @@ -47,7 +47,7 @@ pytest -v ### Run with coverage report ```bash -pytest --cov=videofeed --cov-report=html +pytest --cov=spectrax --cov-report=html ``` ## Test Categories @@ -77,7 +77,7 @@ Common test fixtures are defined in `conftest.py`: ```python import pytest -from videofeed.recorder import RecordingManager +from spectrax.recorder import RecordingManager @pytest.mark.unit @pytest.mark.recording @@ -110,7 +110,7 @@ Tests can be integrated into CI/CD pipelines: - name: Run tests run: | cd video-feed - pytest --cov=videofeed --cov-report=xml + pytest --cov=spectrax --cov-report=xml ``` ## Troubleshooting diff --git a/video-feed/tests/__init__.py b/tests/__init__.py similarity index 100% rename from video-feed/tests/__init__.py rename to tests/__init__.py diff --git a/video-feed/tests/conftest.py b/tests/conftest.py similarity index 87% rename from video-feed/tests/conftest.py rename to tests/conftest.py index b64515d..32d7453 100644 --- a/video-feed/tests/conftest.py +++ b/tests/conftest.py @@ -3,16 +3,12 @@ from __future__ import annotations import os -import sys import tempfile from pathlib import Path from typing import Generator, Optional import pytest -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - @pytest.fixture def temp_dir(): @@ -72,12 +68,12 @@ def sample_config(): def _reset_route_globals() -> None: """Clear module-level set_* globals used by routers.""" - import videofeed.routes.files as files_routes - import videofeed.routes.recordings as recordings_routes - import videofeed.routes.statistics as statistics_routes - import videofeed.routes.video as video_routes - from videofeed import credentials as creds_mod - from videofeed.auth_gate import reset_auth_state, set_secure_cookie, set_signing_key_override + import spectrax.routes.files as files_routes + import spectrax.routes.recordings as recordings_routes + import spectrax.routes.statistics as statistics_routes + import spectrax.routes.video as video_routes + from spectrax import credentials as creds_mod + from spectrax.auth_gate import reset_auth_state, set_secure_cookie, set_signing_key_override if hasattr(files_routes, "reset_files_state"): files_routes.reset_files_state() @@ -125,9 +121,9 @@ def create_test_app( from fastapi import FastAPI, HTTPException, Request from fastapi.responses import JSONResponse - from videofeed.auth_gate import AuthMiddleware, set_secure_cookie, set_signing_key_override - from videofeed import credentials as creds_mod - from videofeed.routes import ( + from spectrax.auth_gate import AuthMiddleware, set_secure_cookie, set_signing_key_override + from spectrax import credentials as creds_mod + from spectrax.routes import ( auth_router, files_router, pages_router, @@ -135,9 +131,9 @@ def create_test_app( statistics_router, video_router, ) - import videofeed.routes.files as files_routes - import videofeed.routes.recordings as recordings_routes - import videofeed.routes.statistics as statistics_routes + import spectrax.routes.files as files_routes + import spectrax.routes.recordings as recordings_routes + import spectrax.routes.statistics as statistics_routes # In-memory secrets for CI (no OS keyring) creds_mod.use_memory_store(True) @@ -183,8 +179,8 @@ async def unhandled_exception_handler(request: Request, exc: Exception): @pytest.fixture def memory_secrets(): """Enable in-memory keyring for a test, then reset.""" - from videofeed import credentials as creds_mod - from videofeed.auth_gate import reset_auth_state, set_signing_key_override + from spectrax import credentials as creds_mod + from spectrax.auth_gate import reset_auth_state, set_signing_key_override store = creds_mod.use_memory_store(True) set_signing_key_override("test-session-signing-key-32bytes!!") @@ -196,7 +192,7 @@ def memory_secrets(): def api_client(test_recordings_dir, test_db_path) -> Generator: """Authenticated TestClient with empty recordings DB.""" from fastapi.testclient import TestClient - from videofeed.api import RecordingsAPI + from spectrax.api import RecordingsAPI _reset_route_globals() @@ -241,7 +237,7 @@ def api_client(test_recordings_dir, test_db_path) -> Generator: def api_client_no_auth(test_recordings_dir, test_db_path) -> Generator: """TestClient without auth middleware (characterization of route bodies).""" from fastapi.testclient import TestClient - from videofeed.api import RecordingsAPI + from spectrax.api import RecordingsAPI _reset_route_globals() api = RecordingsAPI(db_path=test_db_path) diff --git a/video-feed/tests/test_api_characterization.py b/tests/test_api_characterization.py similarity index 97% rename from video-feed/tests/test_api_characterization.py rename to tests/test_api_characterization.py index b6946cf..324bba6 100644 --- a/video-feed/tests/test_api_characterization.py +++ b/tests/test_api_characterization.py @@ -6,7 +6,7 @@ from fastapi.testclient import TestClient from tests.conftest import create_test_app, _reset_route_globals -from videofeed.api import RecordingsAPI +from spectrax.api import RecordingsAPI pytestmark = pytest.mark.api @@ -109,7 +109,7 @@ def test_pages_render(client_with_files): def test_error_body_has_no_path_leak(client_with_files, monkeypatch, test_recordings_dir): """Forced failure must not return absolute paths or exception strings.""" - import videofeed.routes.recordings as rec + import spectrax.routes.recordings as rec def boom(**kwargs): raise RuntimeError(f"sqlite failed at {test_recordings_dir}/secret.db") diff --git a/video-feed/tests/test_auth.py b/tests/test_auth.py similarity index 97% rename from video-feed/tests/test_auth.py rename to tests/test_auth.py index fa78812..8491922 100644 --- a/video-feed/tests/test_auth.py +++ b/tests/test_auth.py @@ -6,9 +6,9 @@ from fastapi.testclient import TestClient from tests.conftest import create_test_app, _reset_route_globals -from videofeed import credentials as creds_mod -from videofeed.api import RecordingsAPI -from videofeed.auth_gate import ( +from spectrax import credentials as creds_mod +from spectrax.api import RecordingsAPI +from spectrax.auth_gate import ( COOKIE_NAME, LOGIN_RATE_LIMIT, hash_api_key, diff --git a/video-feed/tests/test_config_security.py b/tests/test_config_security.py similarity index 90% rename from video-feed/tests/test_config_security.py rename to tests/test_config_security.py index 0bc655b..c34fdd8 100644 --- a/video-feed/tests/test_config_security.py +++ b/tests/test_config_security.py @@ -5,8 +5,8 @@ import pytest -from videofeed.config import create_config, SurveillanceConfig -from videofeed.utils import print_urls +from spectrax.config import create_config, SurveillanceConfig +from spectrax.utils import print_urls pytestmark = pytest.mark.unit @@ -46,7 +46,7 @@ def test_print_urls_redacts_passwords(): # Ensure secrets are not interpolated into URLs in the function body import inspect - from videofeed import utils as u + from spectrax import utils as u src = inspect.getsource(u.print_urls) assert "user:pass@" not in src diff --git a/video-feed/tests/test_db.py b/tests/test_db.py similarity index 100% rename from video-feed/tests/test_db.py rename to tests/test_db.py diff --git a/video-feed/tests/test_db_connection.py b/tests/test_db_connection.py similarity index 84% rename from video-feed/tests/test_db_connection.py rename to tests/test_db_connection.py index 9c120ab..66dec3c 100644 --- a/video-feed/tests/test_db_connection.py +++ b/tests/test_db_connection.py @@ -11,14 +11,10 @@ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger('db-connection-test') -# Add the video-feed directory to sys.path to make videofeed importable -video_feed_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'video-feed') -if video_feed_dir not in sys.path: - sys.path.insert(0, video_feed_dir) # Import relevant modules -from videofeed.api import RecordingsAPI -from videofeed.recorder import RecordingManager +from spectrax.api import RecordingsAPI +from spectrax.recorder import RecordingManager def test_db_connection(): """Test database connection with expanded paths.""" diff --git a/tests/test_package_layout.py b/tests/test_package_layout.py new file mode 100644 index 0000000..7ab2a02 --- /dev/null +++ b/tests/test_package_layout.py @@ -0,0 +1,83 @@ +"""Layout / packaging smoke tests (Phase 1).""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from spectrax import __version__ +from spectrax.paths import ( + default_config_path, + default_tls_paths, + models_dir, + project_root, +) + + +@pytest.mark.unit +def test_package_version(): + assert __version__ == "0.2.0" + + +@pytest.mark.unit +def test_templates_packaged(): + import spectrax + + templates = Path(spectrax.__file__).resolve().parent / "templates" + assert templates.is_dir() + for name in ("login.html", "viewer.html", "recordings.html"): + assert (templates / name).is_file(), f"missing template {name}" + + +@pytest.mark.unit +def test_project_root_finds_pyproject(): + root = project_root() + assert (root / "pyproject.toml").is_file() + text = (root / "pyproject.toml").read_text(encoding="utf-8") + assert 'name = "spectrax"' in text + + +@pytest.mark.unit +def test_default_config_path_prefers_spectrax_yml(): + path = default_config_path() + assert path.name in {"spectrax.yml", "surveillance.yml"} + # In a normal checkout the renamed file exists + if path.name == "spectrax.yml": + assert path.is_file() + + +@pytest.mark.unit +def test_models_dir_under_project_root(): + assert models_dir() == project_root() / "models" + + +@pytest.mark.unit +def test_default_tls_paths_under_project_root(): + key, cert = default_tls_paths() + root = project_root() + assert key == root / "server.key" + assert cert == root / "server.crt" + + +@pytest.mark.unit +def test_keychain_service_unchanged(): + """Renaming the package must not invalidate existing keychain secrets.""" + from spectrax.constants import KEYCHAIN_SERVICE + + assert KEYCHAIN_SERVICE == "video-feed-mediamtx" + + +@pytest.mark.unit +def test_resolve_model_path_uses_models_dir(tmp_path, monkeypatch): + from spectrax import utils as utils_mod + + fake_root = tmp_path / "repo" + models = fake_root / "models" + models.mkdir(parents=True) + weight = models / "yolov8n.pt" + weight.write_bytes(b"fake") + + monkeypatch.setattr(utils_mod, "models_dir", lambda: models) + assert utils_mod.resolve_model_path("yolov8n.pt") == str(weight) + assert utils_mod.resolve_model_path("missing.pt") == "missing.pt" diff --git a/video-feed/tests/test_recording.py b/tests/test_recording.py similarity index 97% rename from video-feed/tests/test_recording.py rename to tests/test_recording.py index 1fe16cc..37ef2bc 100644 --- a/video-feed/tests/test_recording.py +++ b/tests/test_recording.py @@ -9,10 +9,8 @@ import time from pathlib import Path -# Add the video-feed directory to the path -sys.path.insert(0, str(Path(__file__).parent / "video-feed")) -from videofeed.recorder import RecordingManager +from spectrax.recorder import RecordingManager import numpy as np import cv2 diff --git a/video-feed/tests/test_storage_location.py b/tests/test_storage_location.py similarity index 87% rename from video-feed/tests/test_storage_location.py rename to tests/test_storage_location.py index 82352ef..5908c72 100644 --- a/video-feed/tests/test_storage_location.py +++ b/tests/test_storage_location.py @@ -11,15 +11,11 @@ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger('storage-test') -# Add the video-feed directory to sys.path to make videofeed importable -video_feed_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'video-feed') -if video_feed_dir not in sys.path: - sys.path.insert(0, video_feed_dir) # Import relevant modules -from videofeed.recorder import RecordingManager -from videofeed.api import RecordingsAPI -from videofeed.config import SurveillanceConfig +from spectrax.recorder import RecordingManager +from spectrax.api import RecordingsAPI +from spectrax.config import SurveillanceConfig def test_path_expansion(): """Test that paths with ~ are properly expanded.""" diff --git a/video-feed/tests/test_supervision_integration.py b/tests/test_supervision_integration.py similarity index 94% rename from video-feed/tests/test_supervision_integration.py rename to tests/test_supervision_integration.py index a944ac8..afb067c 100644 --- a/video-feed/tests/test_supervision_integration.py +++ b/tests/test_supervision_integration.py @@ -4,8 +4,6 @@ import sys import os -# Add video-feed to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'video-feed')) def test_imports(): """Test that all imports work correctly.""" @@ -15,7 +13,7 @@ def test_imports(): print("✅ Supervision imported successfully") print(f" Version: {sv.__version__ if hasattr(sv, '__version__') else 'unknown'}") - from videofeed.detector import RTSPObjectDetector, DetectorManager + from spectrax.detector import RTSPObjectDetector, DetectorManager print("✅ Detector modules imported successfully") # Test that Supervision components are available @@ -58,7 +56,7 @@ def test_detector_class(): """Test that RTSPObjectDetector class can be instantiated.""" print("\nTesting RTSPObjectDetector class...") try: - from videofeed.detector import RTSPObjectDetector + from spectrax.detector import RTSPObjectDetector # Create a detector instance (without starting it) detector = RTSPObjectDetector( diff --git a/video-feed/pytest.ini b/video-feed/pytest.ini deleted file mode 100644 index d3671a9..0000000 --- a/video-feed/pytest.ini +++ /dev/null @@ -1,41 +0,0 @@ -[pytest] -# Pytest configuration for SentriX surveillance system - -# Test discovery patterns -python_files = test_*.py -python_classes = Test* -python_functions = test_* - -# Test paths -testpaths = tests - -# Output options -addopts = - -v - --strict-markers - --tb=short - --disable-warnings - -# Markers for categorizing tests -markers = - unit: Unit tests for individual components - integration: Integration tests for component interaction - db: Database-related tests - recording: Recording functionality tests - detection: Object detection tests - slow: Tests that take significant time to run - requires_mediamtx: Tests that require MediaMTX to be installed - api: API/route characterization and auth tests (no torch/MediaMTX) - -# Logging -log_cli = false -log_cli_level = INFO -log_cli_format = %(asctime)s [%(levelname)8s] %(message)s -log_cli_date_format = %Y-%m-%d %H:%M:%S - -# Coverage options (if using pytest-cov) -# [coverage:run] -# source = videofeed -# omit = -# */tests/* -# */conftest.py diff --git a/video-feed/setup.py b/video-feed/setup.py deleted file mode 100644 index 39bacf3..0000000 --- a/video-feed/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Setup script for video-feed package.""" - -from setuptools import setup, find_packages - -setup( - name="videofeed", - version="0.2.0", - packages=find_packages(), - install_requires=[ - "typer", - "keyring", - "pyyaml", - "fastapi", - "uvicorn", - "opencv-python-headless", - "torch", - "ultralytics", - "Pillow", - "Jinja2", - ], - entry_points={ - "console_scripts": [ - "videofeed=videofeed.surveillance:app", - "surveillance=videofeed.surveillance:app", - ], - }, - python_requires=">=3.8", - description="Unified surveillance system with RTSP/HLS streaming and YOLO object detection", - author="Perimeter AI", - long_description="A comprehensive surveillance system featuring MediaMTX-based streaming, YOLO object detection, event-based recording, and a web dashboard.", -) diff --git a/video-feed/videofeed/__init__.py b/video-feed/videofeed/__init__.py deleted file mode 100644 index 3fe6e34..0000000 --- a/video-feed/videofeed/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Video feed package for RTSP/HLS streaming.""" - -__version__ = "0.1.0" diff --git a/video-feed/videofeed/cli.py b/video-feed/videofeed/cli.py deleted file mode 100644 index 20ccbc0..0000000 --- a/video-feed/videofeed/cli.py +++ /dev/null @@ -1,31 +0,0 @@ -"""CLI interface for video-feed. - -DEPRECATED: This module is deprecated. Use videofeed.surveillance instead. -All functionality has been moved to the surveillance module for better organization. - -Usage: - python -m videofeed.surveillance run # Instead of: python -m videofeed.cli run - python -m videofeed.surveillance detect # Instead of: python -m videofeed.cli detect - python -m videofeed.surveillance reset # Instead of: python -m videofeed.cli reset -""" - -import typer -import warnings - -# Import the new unified app -from videofeed.surveillance import app as surveillance_app - -# Show deprecation warning -warnings.warn( - "videofeed.cli is deprecated. Use 'python -m videofeed.surveillance' instead. " - "All CLI commands have been moved to the surveillance module.", - DeprecationWarning, - stacklevel=2 -) - -# Re-export the surveillance app for backward compatibility -app = surveillance_app - - -if __name__ == "__main__": - app()