feat(import): user-extensible BOR_IMPORT_EXTENSIONS — any well-formed extension, A9 family stays the default

This commit is contained in:
2026-08-31 22:42:41 -04:00
parent 281f3555c3
commit d94f3d5a52
9 changed files with 485 additions and 47 deletions
+30 -16
View File
@@ -6,18 +6,22 @@ Every setting can be overridden with an environment variable prefixed
from __future__ import annotations
import os
import re
from functools import lru_cache
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
#: The A9 import formats (PLAN anchor A9, revised 2026-08-21; revised
#: 2026-08-27, owner permission — the full Podman quadlet family
#: The built-in DEFAULT import formats (PLAN anchor A9, revised 2026-08-21;
#: revised 2026-08-27, owner permission — the full Podman quadlet family
#: ``container, network, volume, image, pod, kube, swap, os, endpoint``
#: plus Jinja templates ``j2`` join the allowed set, chunked as plain
#: text). ``BOR_IMPORT_EXTENSIONS`` may narrow — but never widen — this
#: set.
_ALLOWED_IMPORT_EXTENSIONS: frozenset[str] = frozenset(
#: plus Jinja templates ``j2`` join the default, chunked as plain text).
#: This is the default scope AND the ``.env.example`` example — it is NOT
#: a ceiling: ``BOR_IMPORT_EXTENSIONS`` may name **any** well-formed
#: extension (lowercase letters/digits, no dot) or narrow to a subset
#: (owner permission 2026-08-31, phase 56); see
#: :py:attr:`Settings.import_extensions`.
_DEFAULT_IMPORT_EXTENSIONS: frozenset[str] = frozenset(
{
"md", "markdown", "txt", "yaml", "yml", "json", "py",
# A9 revised 2026-08-27 (owner permission): quadlet family + jinja.
@@ -136,13 +140,18 @@ class Settings(BaseSettings):
session_max_age: int = 43_200
session_cookie: str = "bor_session"
# --- Import scope (A9, revised 2026-08-21 and 2026-08-27) ---
# --- Import scope (A9 default; any extension allowed — phase 56) ---
# Comma-separated list of lowercased file extensions (no dot) imported
# by ``scripts/import_docs.py``. Hidden (dot) path components are always
# by ``scripts/import_docs.py``. **Any** well-formed extension is
# allowed (lowercase letters/digits, 1-16 chars — the shape guard
# doubles as the typo guard); the value below is the built-in default
# (the A9 family, incl. the quadlet family + ``j2``) and the documented
# example in ``.env.example``. Hidden (dot) path components are always
# skipped, plus the importer's exclusion list.
# Stored as a raw CSV string (env-native — no JSON) and parsed on demand
# via :py:meth:`import_extension_set`. ``mode="after"`` validation runs
# against the raw string so a typo fails loudly at startup.
# via :py:meth:`import_extension_set`. The validator rejects an empty
# list and malformed tokens so a typo fails loudly at startup (it can
# no longer reject a novel extension).
import_extensions: str = (
"md,markdown,txt,yaml,yml,json,py,"
"container,network,volume,image,pod,kube,swap,os,endpoint,j2"
@@ -172,16 +181,21 @@ class Settings(BaseSettings):
@field_validator("import_extensions")
@classmethod
def _import_extensions_known(cls, v: str) -> str:
"""Reject unknown/empty formats loudly instead of silently importing
nothing (a typo like ``md,jsonn`` would otherwise walk zero files)."""
"""Reject an empty list or malformed tokens loudly instead of
silently importing nothing (a typo like ``md,jsonn`` would
otherwise walk zero files). Any well-formed extension is accepted —
the A9 family is the default, not a ceiling (owner permission
2026-08-31, phase 56)."""
exts = {part.strip().lstrip(".").lower() for part in v.split(",") if part.strip()}
if not exts:
raise ValueError("import_extensions must name at least one format")
unknown = exts - _ALLOWED_IMPORT_EXTENSIONS
if unknown:
malformed = sorted(
ext for ext in exts if re.fullmatch(r"[a-z0-9]{1,16}", ext) is None
)
if malformed:
raise ValueError(
f"unknown import extension(s): {', '.join(sorted(unknown))} — "
f"allowed: {', '.join(sorted(_ALLOWED_IMPORT_EXTENSIONS))}"
f"import_extensions contains malformed token(s): {', '.join(malformed)} — "
"each extension must be lowercase letters/digits only, 1-16 chars, no dot"
)
return v