chore: startup path anchoring, env parsing and gitignore

- Launcher guards Python 3.13 before importing the package and chdirs to the
  repo root so cwd-relative default paths resolve consistently.
- main() logs the resolved .env path and warns on a missing static dir or a
  short auth token; env_file reads utf-8-sig so a BOM doesn't drop the first
  variable.
- .gitignore covers .claude/settings.local.json and .impeccable/.
This commit is contained in:
changukyu 2026-07-11 16:07:49 +09:00
parent 3535bfee15
commit 6ab6da3719
4 changed files with 38 additions and 3 deletions

4
.gitignore vendored
View file

@ -31,6 +31,10 @@ artifacts/
tmp_*
tmp_dbg_img/
# Local tool state (do not rely on the machine-global gitignore)
.claude/settings.local.json
.impeccable/
# OS
.DS_Store
Thumbs.db

View file

@ -1,6 +1,14 @@
from pathlib import Path
import os
import sys
# Guard BEFORE importing rights_filter so a 3.13-only syntax in the package
# surfaces this friendly message instead of a raw SyntaxError/ImportError.
if sys.version_info < (3, 13):
sys.exit(
"Copyrighter requires Python 3.13+ — activate the offline venv "
"(see docs/operations/offline-install.md)."
)
ROOT = Path(__file__).resolve().parent
SRC = ROOT / "src"
@ -12,4 +20,8 @@ from rights_filter.server.__main__ import main
if __name__ == "__main__":
# Anchor the working directory to the repo root so the cwd-relative default
# paths (data/, web/operator-gui) resolve the same regardless of where the
# launcher was invoked from.
os.chdir(ROOT)
main()

View file

@ -33,7 +33,12 @@ def main() -> None:
parser.add_argument("--env", default=".env")
args = parser.parse_args()
load_env_file(Path(args.env), os.environ)
env_path = Path(args.env)
load_env_file(env_path, os.environ)
# Log the resolved env path (and whether it was found) so an operator running
# from an unexpected directory can see which .env — if any — was actually
# loaded, rather than silently getting no token / disabled providers.
print(f".env: {env_path.resolve()} ({'loaded' if env_path.exists() else 'not found'})")
provider_runtime = build_provider_runtime(os.environ)
print("Provider readiness: " + _provider_readiness(provider_runtime))
image_store = LocalSubmissionImageStore(Path(args.images))
@ -44,13 +49,25 @@ def main() -> None:
auth_token = os.environ.get("COPYRIGHTER_AUTH_TOKEN", "").strip() or None
if auth_token is None:
print("WARNING: COPYRIGHTER_AUTH_TOKEN not set — data routes are unauthenticated.")
elif len(auth_token) < 32:
print(
"WARNING: COPYRIGHTER_AUTH_TOKEN is shorter than 32 characters — "
"use a longer, high-entropy token (e.g. `python -c \"import secrets; print(secrets.token_urlsafe(32))\"`)."
)
static_path = Path(args.static)
if not static_path.exists():
print(
f"WARNING: static GUI directory not found: {static_path.resolve()} — "
"the operator UI will return 404. Pass --static or run from the repo root."
)
server = build_server(
host=args.host,
port=args.port,
store=store,
image_store=image_store,
static_dir=Path(args.static),
static_dir=static_path,
auth_token=auth_token,
)
print(f"Copyrighter API server listening on http://{args.host}:{args.port}")

View file

@ -9,7 +9,9 @@ def load_env_file(path: Path | str, environ: MutableMapping[str, str]) -> None:
if not env_path.exists():
return
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
# utf-8-sig so a BOM on the file doesn't get glued to the first key (which
# would silently drop the first variable — it would never match its name).
for raw_line in env_path.read_text(encoding="utf-8-sig").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue