"""Most do modułów opcjonalnych — CELOWO nie zna ani jednej ich nazwy. Ten plik jest w KAŻDYM obrazie, także tam, gdzie nie ma żadnego modułu. Gdyby wymieniał moduły z nazwy, nazwa nieobecnej funkcji i tak jechałaby do obrazu, w którym tej funkcji nie ma — czyli dokładnie ten ślad, którego całe wydzielanie ma się pozbyć. Dlatego moduły są ODKRYWANE: most przechodzi po podkatalogach `app/moduly/` i pyta każdy, co wnosi. Gdy nie ma żadnego, wszystkie odpowiedzi są puste, a aplikacja zachowuje się tak, jakby takiego pojęcia nigdy nie było — nie jak aplikacja z wyłączoną funkcją. """ from __future__ import annotations import importlib import pkgutil from typing import Any def _znalezione() -> list[Any]: """Moduły obecne w tym obrazie, w kolejności alfabetycznej katalogów. Kolejność jest stała, żeby dwa obrazy z tym samym zestawem zachowywały się identycznie — inaczej wynik zależałby od kolejności odczytu katalogu.""" try: paczka = importlib.import_module("app.moduly") except ImportError: return [] znalezione = [] for info in sorted(pkgutil.iter_modules(paczka.__path__), key=lambda i: i.name): znalezione.append(importlib.import_module(f"app.moduly.{info.name}")) return znalezione _MODULY = _znalezione() DOSTEPNE = bool(_MODULY) def _zbierz(nazwa: str, domyslne): """Wartość atrybutu z każdego modułu, który go ma.""" return [getattr(m, nazwa) for m in _MODULY if hasattr(m, nazwa)] or domyslne def funkcje() -> tuple[tuple[str, str, str, str], ...]: """Pozycje do katalogu funkcji — jako surowe krotki, nie obiekty. Surowe, bo katalog buduje `features`, a gdyby moduł importował stamtąd klasę, powstałby cykl: features → most → moduł → features.""" out: list[tuple[str, str, str, str]] = [] for wartosc in _zbierz("FUNKCJA", []): out.extend(wartosc) return tuple(out) def trasy() -> dict[tuple[str, str], str]: out: dict[tuple[str, str], str] = {} for wartosc in _zbierz("TRASY", []): out.update(wartosc) return out def zasoby() -> dict[str, Any]: out: dict[str, Any] = {} for wartosc in _zbierz("ZASOBY", []): out.update(wartosc) return out def akcje() -> dict[str, str]: """Akcje formularza wnoszone przez moduły: nazwa → wymagane uprawnienie.""" out: dict[str, str] = {} for wartosc in _zbierz("AKCJE", []): out.update(wartosc) return out def pola_formularza(): """Zależność FastAPI deklarująca dodatkowe pola formularza. Pola muszą być w SYGNATURZE, żeby FastAPI je odczytało, a sygnatura wspólnego handlera nie może ich wymieniać. Zależność przenosi deklarację do modułu. Gdy pola deklaruje więcej niż jeden moduł, trzeba je złożyć — a składanie sygnatur w locie to kod, który psuje się cicho. Póki nikt tego nie potrzebuje, mówimy o tym wprost przy starcie, zamiast budować machinerię na zapas. """ deklarujace = _zbierz("pola", []) if not deklarujace: return lambda: {} if len(deklarujace) > 1: raise RuntimeError( "Więcej niż jeden moduł deklaruje pola formularza. Trzeba je złożyć " "w jedną zależność — patrz komentarz w app/rozszerzenia.py.") return deklarujace[0] def kontekst_szablonu(request) -> dict: """Co moduły dokładają do kontekstu szablonu. Bez modułów — nic.""" out: dict = {} for funkcja in _zbierz("katalog_dla", []): out.update(funkcja(request)) return out def obsluz(akcja: str, **dane): """Wykonanie akcji modułu. None = żaden się do niej nie przyznaje. Moduł może oddać słownik (trafi do kontekstu szablonu) albo gotową odpowiedź HTTP — na przykład plik do pobrania, który nie jest stroną.""" for funkcja in _zbierz("obsluz", []): wynik = funkcja(akcja, **dane) if wynik is not None: return wynik return None def zamontuj(app, **zaleznosci) -> None: """Podpięcie tras modułów i przekazanie im tego, czego potrzebują.""" for funkcja in _zbierz("zamontuj", []): funkcja(app, **zaleznosci) def _katalogi(nazwa: str) -> list[str]: import pathlib return [str(pathlib.Path(m.__file__).parent / nazwa) for m in _MODULY if (pathlib.Path(m.__file__).parent / nazwa).is_dir()] def katalog_szablonow() -> list[str]: """Katalogi szablonów modułów — nakładane PRZED wspólnymi.""" return _katalogi("templates") def katalog_zasobow() -> list[str]: return _katalogi("static")