Move secrets and URLs out of source into .env
All checks were successful
lint / ruff (push) Successful in 45s
All checks were successful
lint / ruff (push) Successful in 45s
Adds a stdlib-only loader (src/lib/env.py) that walks up to find the nearest .env. display.py and notebooks/_helpers.py now `require()` the config values; wifi-check.sh sources .env to derive its probe host. The .env file is gitignored; .env.example documents the required keys. The existing tokens are still present in git history and will be scrubbed in the next commit; rotate them after the rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8609b4a884
commit
90b692da39
8 changed files with 69 additions and 19 deletions
4
.env.example
Normal file
4
.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
IMMICH_URL=https://immich.example.com
|
||||
IMMICH_API_KEY=your-immich-api-key
|
||||
HA_URL=https://homeassistant.example.com
|
||||
HA_TOKEN=your-home-assistant-long-lived-token
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@ __pycache__/
|
|||
*.egg-info/
|
||||
.ipynb_checkpoints/
|
||||
photo_history.json
|
||||
.env
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ former `dither_test/`):
|
|||
- **Display refresh takes 12-15 seconds** — the BUSY pin polling handles this
|
||||
- **No test suite** — this is a hardware project; test by deploying to the Pi
|
||||
- **Dependencies on Pi**: `python3-pil python3-numba python3-smbus spidev gpiozero`
|
||||
- **Config via environment variables**: `IMMICH_URL`, `IMMICH_API_KEY`, `HA_URL`, `HA_TOKEN` (with hardcoded defaults in display.py)
|
||||
- **Config via `.env`** (gitignored): `IMMICH_URL`, `IMMICH_API_KEY`, `HA_URL`, `HA_TOKEN`. Loaded by `src/lib/env.py` (stdlib-only); `require(key)` raises if a value is missing. Copy `.env.example` to `.env` and fill in. The Pi keeps its own `~/frame/.env` — `sync.sh` excludes it.
|
||||
- **Uses only stdlib `urllib`** — no requests library; the Immich client uses `urllib.request` directly
|
||||
- **Single-instance lock** at `/tmp/frame.lock` (fcntl) — overlapping cron runs exit cleanly
|
||||
- `sys.path.append` is used to add `lib/` to the path from display.py
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from __future__ import annotations
|
|||
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -21,8 +20,6 @@ REPO = Path(__file__).resolve().parent.parent
|
|||
CACHE_DIR = Path(tempfile.gettempdir()) / "frame_notebook"
|
||||
|
||||
DEFAULT_PEOPLE = ("Me", "Ruby")
|
||||
DEFAULT_IMMICH_URL = "https://immich.example.com"
|
||||
DEFAULT_IMMICH_API_KEY = "REDACTED_IMMICH_API_KEY"
|
||||
|
||||
|
||||
def bootstrap() -> None:
|
||||
|
|
@ -32,15 +29,16 @@ def bootstrap() -> None:
|
|||
if sp not in sys.path:
|
||||
sys.path.insert(0, sp)
|
||||
sys.modules.setdefault("waveshare_epd.epdconfig", ModuleType("waveshare_epd.epdconfig"))
|
||||
from env import load_env
|
||||
|
||||
load_env()
|
||||
|
||||
|
||||
def immich_client():
|
||||
from env import require
|
||||
from immich import ImmichClient
|
||||
|
||||
return ImmichClient(
|
||||
os.environ.get("IMMICH_URL", DEFAULT_IMMICH_URL),
|
||||
os.environ.get("IMMICH_API_KEY", DEFAULT_IMMICH_API_KEY),
|
||||
)
|
||||
return ImmichClient(require("IMMICH_URL"), require("IMMICH_API_KEY"))
|
||||
|
||||
|
||||
def is_landscape(asset: dict) -> bool:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import fcntl
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
|
@ -10,6 +9,7 @@ from PIL import Image
|
|||
|
||||
sys.path.append(str(Path(__file__).parent / "lib"))
|
||||
from crop import face_aware_crop
|
||||
from env import load_env, require
|
||||
from homeassistant import HomeAssistantClient
|
||||
from immich import ImmichClient, get_random_photo_from_album, get_random_photo_of_people
|
||||
from overlay import format_age, format_location
|
||||
|
|
@ -18,14 +18,11 @@ from overlay import format_age, format_location
|
|||
# GPIO pins at import time, so two overlapping invocations would both crash
|
||||
# on "GPIO busy" before reaching the flock below.
|
||||
|
||||
IMMICH_URL = os.environ.get("IMMICH_URL", "https://immich.example.com")
|
||||
IMMICH_API_KEY = os.environ.get("IMMICH_API_KEY", "REDACTED_IMMICH_API_KEY")
|
||||
|
||||
HA_URL = os.environ.get("HA_URL", "https://homeassistant.example.com")
|
||||
HA_TOKEN = os.environ.get(
|
||||
"HA_TOKEN",
|
||||
"REDACTED_HA_TOKEN",
|
||||
)
|
||||
load_env()
|
||||
IMMICH_URL = require("IMMICH_URL")
|
||||
IMMICH_API_KEY = require("IMMICH_API_KEY")
|
||||
HA_URL = require("HA_URL")
|
||||
HA_TOKEN = require("HA_TOKEN")
|
||||
HA_PRESENCE = {"Andras": "person.andras", "Ruby": "person.ruby"}
|
||||
|
||||
|
||||
|
|
|
|||
43
src/lib/env.py
Normal file
43
src/lib/env.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""Minimal stdlib .env loader.
|
||||
|
||||
Reads KEY=VALUE lines from a .env file (project root by default) into
|
||||
`os.environ`, leaving already-set variables untouched.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _find_env() -> Path | None:
|
||||
# Walk up from this file and from cwd; first .env wins. Handles both the
|
||||
# dev layout (src/lib/env.py with .env at repo root) and the Pi layout
|
||||
# (lib/env.py with .env at ~/frame/.env).
|
||||
for start in (Path(__file__).resolve().parent, Path.cwd().resolve()):
|
||||
for d in (start, *start.parents):
|
||||
candidate = d / ".env"
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def load_env(path: Path | None = None) -> None:
|
||||
env_path = path or _find_env()
|
||||
if env_path is None or not env_path.exists():
|
||||
return
|
||||
for raw in env_path.read_text().splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
os.environ.setdefault(key, value)
|
||||
|
||||
|
||||
def require(key: str) -> str:
|
||||
value = os.environ.get(key)
|
||||
if not value:
|
||||
raise RuntimeError(f"missing required env var: {key} (set it in .env or the environment)")
|
||||
return value
|
||||
|
|
@ -4,7 +4,13 @@
|
|||
# brcmfmac chip on the Pi Zero 2W.
|
||||
|
||||
CONNECTION="netplan-wlan0-HiddenPlace"
|
||||
PROBE_HOST="homeassistant.example.com"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
[ -f "$SCRIPT_DIR/.env" ] && set -a && . "$SCRIPT_DIR/.env" && set +a
|
||||
PROBE_HOST="${HA_URL#http*://}"
|
||||
PROBE_HOST="${PROBE_HOST%%/*}"
|
||||
: "${PROBE_HOST:?HA_URL must be set in .env}"
|
||||
|
||||
probe() {
|
||||
ping -c 1 -W 5 192.168.0.1 >/dev/null 2>&1 \
|
||||
|
|
|
|||
2
sync.sh
2
sync.sh
|
|
@ -1,2 +1,2 @@
|
|||
#!/bin/bash
|
||||
rsync -avz --progress src/ andras@192.168.0.81:~/frame/
|
||||
rsync -avz --progress --exclude=.env src/ andras@192.168.0.81:~/frame/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue