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/
|
*.egg-info/
|
||||||
.ipynb_checkpoints/
|
.ipynb_checkpoints/
|
||||||
photo_history.json
|
photo_history.json
|
||||||
|
.env
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ former `dither_test/`):
|
||||||
- **Display refresh takes 12-15 seconds** — the BUSY pin polling handles this
|
- **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
|
- **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`
|
- **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
|
- **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
|
- **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
|
- `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 contextlib
|
||||||
import io
|
import io
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
@ -21,8 +20,6 @@ REPO = Path(__file__).resolve().parent.parent
|
||||||
CACHE_DIR = Path(tempfile.gettempdir()) / "frame_notebook"
|
CACHE_DIR = Path(tempfile.gettempdir()) / "frame_notebook"
|
||||||
|
|
||||||
DEFAULT_PEOPLE = ("Me", "Ruby")
|
DEFAULT_PEOPLE = ("Me", "Ruby")
|
||||||
DEFAULT_IMMICH_URL = "https://immich.example.com"
|
|
||||||
DEFAULT_IMMICH_API_KEY = "REDACTED_IMMICH_API_KEY"
|
|
||||||
|
|
||||||
|
|
||||||
def bootstrap() -> None:
|
def bootstrap() -> None:
|
||||||
|
|
@ -32,15 +29,16 @@ def bootstrap() -> None:
|
||||||
if sp not in sys.path:
|
if sp not in sys.path:
|
||||||
sys.path.insert(0, sp)
|
sys.path.insert(0, sp)
|
||||||
sys.modules.setdefault("waveshare_epd.epdconfig", ModuleType("waveshare_epd.epdconfig"))
|
sys.modules.setdefault("waveshare_epd.epdconfig", ModuleType("waveshare_epd.epdconfig"))
|
||||||
|
from env import load_env
|
||||||
|
|
||||||
|
load_env()
|
||||||
|
|
||||||
|
|
||||||
def immich_client():
|
def immich_client():
|
||||||
|
from env import require
|
||||||
from immich import ImmichClient
|
from immich import ImmichClient
|
||||||
|
|
||||||
return ImmichClient(
|
return ImmichClient(require("IMMICH_URL"), require("IMMICH_API_KEY"))
|
||||||
os.environ.get("IMMICH_URL", DEFAULT_IMMICH_URL),
|
|
||||||
os.environ.get("IMMICH_API_KEY", DEFAULT_IMMICH_API_KEY),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_landscape(asset: dict) -> bool:
|
def is_landscape(asset: dict) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import fcntl
|
import fcntl
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -10,6 +9,7 @@ from PIL import Image
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).parent / "lib"))
|
sys.path.append(str(Path(__file__).parent / "lib"))
|
||||||
from crop import face_aware_crop
|
from crop import face_aware_crop
|
||||||
|
from env import load_env, require
|
||||||
from homeassistant import HomeAssistantClient
|
from homeassistant import HomeAssistantClient
|
||||||
from immich import ImmichClient, get_random_photo_from_album, get_random_photo_of_people
|
from immich import ImmichClient, get_random_photo_from_album, get_random_photo_of_people
|
||||||
from overlay import format_age, format_location
|
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
|
# GPIO pins at import time, so two overlapping invocations would both crash
|
||||||
# on "GPIO busy" before reaching the flock below.
|
# on "GPIO busy" before reaching the flock below.
|
||||||
|
|
||||||
IMMICH_URL = os.environ.get("IMMICH_URL", "https://immich.example.com")
|
load_env()
|
||||||
IMMICH_API_KEY = os.environ.get("IMMICH_API_KEY", "REDACTED_IMMICH_API_KEY")
|
IMMICH_URL = require("IMMICH_URL")
|
||||||
|
IMMICH_API_KEY = require("IMMICH_API_KEY")
|
||||||
HA_URL = os.environ.get("HA_URL", "https://homeassistant.example.com")
|
HA_URL = require("HA_URL")
|
||||||
HA_TOKEN = os.environ.get(
|
HA_TOKEN = require("HA_TOKEN")
|
||||||
"HA_TOKEN",
|
|
||||||
"REDACTED_HA_TOKEN",
|
|
||||||
)
|
|
||||||
HA_PRESENCE = {"Andras": "person.andras", "Ruby": "person.ruby"}
|
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.
|
# brcmfmac chip on the Pi Zero 2W.
|
||||||
|
|
||||||
CONNECTION="netplan-wlan0-HiddenPlace"
|
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() {
|
probe() {
|
||||||
ping -c 1 -W 5 192.168.0.1 >/dev/null 2>&1 \
|
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
|
#!/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