AI Implementation feature(1089): Scheduled Execution with Cron and Retry Handling (#9)
This commit was merged in pull request #9.
This commit is contained in:
1 parent
3545b81546
commit
5f6ddf76a4
27 files changed
+644
-272
No files matched your search
+47
-19
@@ -13,10 +13,10 @@ from dotenv import dotenv_values, load_dotenv
|
||||
REQUIRED_KEYS = (
|
||||
"MASTODON_BASE_URL",
|
||||
"MASTODON_ACCESS_TOKEN",
|
||||
"VISIBILITY",
|
||||
"MASTODON_VISIBILITY",
|
||||
"SITE_URL",
|
||||
"HASHTAGS",
|
||||
"THROWNBACK_PREFIX",
|
||||
"THROWBACK_PREFIX",
|
||||
"MAX_RETRIES",
|
||||
"RUN_AT",
|
||||
"TZ",
|
||||
@@ -25,11 +25,7 @@ REQUIRED_KEYS = (
|
||||
OPTIONAL_KEYS: tuple[str, ...] = ("BLOG_REPO_URL", "BLOG_DIR")
|
||||
|
||||
DEFAULTS = {
|
||||
"VISIBILITY": "public",
|
||||
"THROWNBACK_PREFIX": "Heute vor 10 Jahren:",
|
||||
"MAX_RETRIES": "3",
|
||||
"TZ": "Europe/Berlin",
|
||||
"RUN_AT": "09:00",
|
||||
"MAX_RETRIES": "5",
|
||||
"BLOG_REPO_URL": "https://git.chaospott.de/Chaospott/site",
|
||||
"BLOG_DIR": "blog",
|
||||
}
|
||||
@@ -46,7 +42,7 @@ class ConfigError(ValueError):
|
||||
class Config:
|
||||
mastodon_base_url: str
|
||||
mastodon_access_token: str
|
||||
visibility: str
|
||||
mastodon_visibility: str
|
||||
site_url: str
|
||||
hashtags: str
|
||||
throwback_prefix: str
|
||||
@@ -67,6 +63,9 @@ class Config:
|
||||
return self.run_at.split(":", 1)[1]
|
||||
|
||||
|
||||
DEFAULT_DOTENV_PATH = Path("/app/.env")
|
||||
|
||||
|
||||
def _read_dotenv(dotenv_path: Optional[Path]) -> dict[str, str]:
|
||||
if dotenv_path is None:
|
||||
return {}
|
||||
@@ -88,19 +87,38 @@ def _values_from_env() -> dict[str, str]:
|
||||
def load_config(dotenv_path: Optional[Path] = None) -> Config:
|
||||
"""Load configuration from a dotenv file and the process environment.
|
||||
|
||||
Environment variables take precedence over the dotenv file so a mounted
|
||||
`.env` can be supplemented by Compose-level overrides.
|
||||
The .env file is loaded via python-dotenv at startup so the bind-mounted
|
||||
runtime secrets are visible to the process. Environment variables take
|
||||
precedence over the dotenv file so a mounted `.env` can be supplemented
|
||||
by Compose-level overrides.
|
||||
|
||||
Required keys are validated against the **raw** merged map (dotenv +
|
||||
process env) before any defaults are applied. Defaults are only used
|
||||
for the retry budget and the optional Jekyll blog source so a missing
|
||||
Mastodon/secret/URL/etc. fails fast with a clear error.
|
||||
"""
|
||||
if dotenv_path is None:
|
||||
dotenv_path = DEFAULT_DOTENV_PATH if DEFAULT_DOTENV_PATH.exists() else None
|
||||
|
||||
if dotenv_path is not None:
|
||||
load_dotenv(dotenv_path=str(dotenv_path), override=False)
|
||||
|
||||
file_values = _read_dotenv(dotenv_path)
|
||||
env_values = _values_from_env()
|
||||
|
||||
merged: dict[str, str] = {}
|
||||
merged.update(file_values)
|
||||
merged.update(env_values)
|
||||
raw: dict[str, str] = {}
|
||||
raw.update(file_values)
|
||||
raw.update(env_values)
|
||||
|
||||
# MAX_RETRIES is the only required key that has a permitted default
|
||||
# (5 attempts per the Job). Apply it before the required-key check so a
|
||||
# missing value uses the documented default rather than failing fast.
|
||||
if not raw.get("MAX_RETRIES"):
|
||||
raw["MAX_RETRIES"] = DEFAULTS["MAX_RETRIES"]
|
||||
|
||||
validate_required(raw)
|
||||
|
||||
merged = dict(raw)
|
||||
apply_defaults(merged)
|
||||
|
||||
validate_config(merged)
|
||||
@@ -119,10 +137,10 @@ def load_config(dotenv_path: Optional[Path] = None) -> Config:
|
||||
return Config(
|
||||
mastodon_base_url=merged["MASTODON_BASE_URL"],
|
||||
mastodon_access_token=merged["MASTODON_ACCESS_TOKEN"],
|
||||
visibility=merged["VISIBILITY"],
|
||||
mastodon_visibility=merged["MASTODON_VISIBILITY"],
|
||||
site_url=merged["SITE_URL"],
|
||||
hashtags=merged["HASHTAGS"],
|
||||
throwback_prefix=merged["THROWNBACK_PREFIX"],
|
||||
throwback_prefix=merged["THROWBACK_PREFIX"],
|
||||
max_retries=_parse_max_retries(merged["MAX_RETRIES"]),
|
||||
run_at=merged["RUN_AT"],
|
||||
tz=merged["TZ"],
|
||||
@@ -138,13 +156,23 @@ def apply_defaults(values: dict[str, str]) -> None:
|
||||
values.setdefault(key, default)
|
||||
|
||||
|
||||
def validate_config(values: dict[str, str]) -> None:
|
||||
errors: list[str] = []
|
||||
def validate_required(values: dict[str, str]) -> None:
|
||||
"""Validate that every required key is present and non-empty in ``values``.
|
||||
|
||||
Called against the raw (no-default) merged map so a missing key always
|
||||
surfaces as a startup error rather than being silently substituted.
|
||||
"""
|
||||
errors: list[str] = []
|
||||
for key in REQUIRED_KEYS:
|
||||
raw = values.get(key, "")
|
||||
if not raw:
|
||||
errors.append(f"missing required configuration key: {key}")
|
||||
if errors:
|
||||
raise ConfigError("; ".join(errors))
|
||||
|
||||
|
||||
def validate_config(values: dict[str, str]) -> None:
|
||||
errors: list[str] = []
|
||||
|
||||
run_at = values.get("RUN_AT", "")
|
||||
if run_at and not _is_valid_hhmm(run_at):
|
||||
@@ -158,10 +186,10 @@ def validate_config(values: dict[str, str]) -> None:
|
||||
if site_url and not _is_valid_url(site_url):
|
||||
errors.append(f"SITE_URL={site_url!r} must be a valid http(s) URL")
|
||||
|
||||
visibility = values.get("VISIBILITY", "")
|
||||
visibility = values.get("MASTODON_VISIBILITY", "")
|
||||
if visibility and not _is_valid_visibility(visibility):
|
||||
errors.append(
|
||||
f"VISIBILITY={visibility!r} must be one of: {sorted(ALLOWED_VISIBILITY)}"
|
||||
f"MASTODON_VISIBILITY={visibility!r} must be one of: {sorted(ALLOWED_VISIBILITY)}"
|
||||
)
|
||||
|
||||
blog_repo_url = values.get("BLOG_REPO_URL", "")
|
||||
|
||||
Reference in new issue
Block a user