Some checks failed
build-and-deploy / build (push) Failing after 5s
- camera_scraper: public-directory-only discovery (Insecam-style HTML + plain-text lists), hard private-range guard (fail closed), per-host rate limiting, Nominatim geocoding at <=1 req/s, TTL'd local snapshot cache, sha256 url_hash dedupe with Postgres upsert - cameras table (migration 002) + /api/cameras?bbox= + snapshot endpoint with cache passthrough in main.py - run_camera_service: long-running cycle worker following NATS->ingester pattern; publishes events.camera for shared ingester - docker-compose camera-service profile, .env.example knobs Verified E2E against TimescaleDB+PostGIS: private-range entries dropped, dedupe across cycles holds, bbox query returns expected rows.
43 lines
1.6 KiB
Python
43 lines
1.6 KiB
Python
"""cameras table for open-camera discovery
|
|
|
|
Revision ID: 002_cameras
|
|
Revises: 001_initial
|
|
Create Date: 2026-08-24
|
|
"""
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
|
|
revision = '002_cameras'
|
|
down_revision = '001_initial'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
'cameras',
|
|
sa.Column('id', UUID(as_uuid=True), primary_key=True),
|
|
sa.Column('url_hash', sa.String(64), nullable=False),
|
|
sa.Column('source_url', sa.Text(), nullable=False),
|
|
sa.Column('snapshot_url', sa.Text()),
|
|
sa.Column('discovery_source', sa.String(128), nullable=False),
|
|
sa.Column('location_lat', sa.Float()),
|
|
sa.Column('location_lon', sa.Float()),
|
|
sa.Column('location_name', sa.String(512)),
|
|
sa.Column('vendor', sa.String(128)),
|
|
sa.Column('device_type', sa.String(64)),
|
|
sa.Column('first_seen', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.Column('last_seen', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.Column('raw', sa.JSON()),
|
|
)
|
|
op.create_index('ix_cameras_location', 'cameras', ['location_lat', 'location_lon'])
|
|
op.create_index('ix_cameras_last_seen', 'cameras', ['last_seen'])
|
|
# Dedupe key. NOTE: cameras is intentionally NOT a Timescale hypertable —
|
|
# it holds current state per camera (one row per url_hash), not time-series.
|
|
op.create_index('ix_cameras_url_hash', 'cameras', ['url_hash'], unique=True)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table('cameras')
|