feat(import): user-extensible BOR_IMPORT_EXTENSIONS — any well-formed extension, A9 family stays the default
This commit is contained in:
+30
-16
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user