simple-module-hosting 0.0.28__tar.gz → 0.0.30__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/PKG-INFO +3 -3
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/pyproject.toml +3 -3
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/app_builder.py +8 -1
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/assets.py +64 -10
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/bootstrap_settings.py +22 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/host_cli.py +5 -3
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_app.py +7 -7
- simple_module_hosting-0.0.30/tests/test_auth_provider_setting.py +59 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/.gitignore +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/LICENSE +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/README.md +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/__init__.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/__main__.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/_error_handlers.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/_host_services.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/_hydrate_step.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/_inertia_setup.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/_inertia_shared.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/_observability.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/_phase_helpers.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/health.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/host_settings.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/i18n_deps.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/i18n_middleware.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/inertia_deps.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/inertia_utils.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/logging.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/manifest.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/middleware.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/migrations.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/permissions.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/py.typed +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/redirects.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/settings.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/shared_props.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/static_files.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_branding_head.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_check_migrations.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_design_packs_wiring.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_error_page_shared_props.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_health.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_host_cli.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_hosting_permissions.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_inertia_i18n_shared_props.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_inertia_manifest.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_inertia_shared_providers.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_lifespan_order.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_locale_middleware.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_logging.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_manifest.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_menu_urls_are_canonical.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_middleware_order.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_precompressed_static.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_proxy_headers.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_redirects.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_response_compression.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_session_cookie_security.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_settings_i18n.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_settings_secrets.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_static_caching.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_strict_discovery_wiring.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_tenant_middleware.py +0 -0
- {simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_translator_dep.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_hosting
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.30
|
|
4
4
|
Summary: FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -27,8 +27,8 @@ Requires-Dist: fastapi>=0.115
|
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
28
|
Requires-Dist: itsdangerous>=2.0
|
|
29
29
|
Requires-Dist: jinja2>=3.1
|
|
30
|
-
Requires-Dist: simple-module-core==0.0.
|
|
31
|
-
Requires-Dist: simple-module-db==0.0.
|
|
30
|
+
Requires-Dist: simple-module-core==0.0.30
|
|
31
|
+
Requires-Dist: simple-module-db==0.0.30
|
|
32
32
|
Requires-Dist: starlette>=0.44
|
|
33
33
|
Requires-Dist: tomlkit>=0.13
|
|
34
34
|
Requires-Dist: uvicorn[standard]>=0.34
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_hosting"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.30"
|
|
4
4
|
description = "FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -30,8 +30,8 @@ dependencies = [
|
|
|
30
30
|
# it directly — out-of-tree hosts have nothing else pulling it in.
|
|
31
31
|
"itsdangerous>=2.0",
|
|
32
32
|
"jinja2>=3.1",
|
|
33
|
-
"simple_module_core==0.0.
|
|
34
|
-
"simple_module_db==0.0.
|
|
33
|
+
"simple_module_core==0.0.30",
|
|
34
|
+
"simple_module_db==0.0.30",
|
|
35
35
|
"starlette>=0.44",
|
|
36
36
|
"tomlkit>=0.13",
|
|
37
37
|
"uvicorn[standard]>=0.34",
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/app_builder.py
RENAMED
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from fastapi import FastAPI
|
|
13
13
|
from simple_module_core.design_packs import DesignPackRegistry
|
|
14
14
|
from simple_module_core.diagnostics import DiagnosticLevel, print_diagnostics, run_diagnostics
|
|
15
|
-
from simple_module_core.discovery import discover_modules, topological_sort
|
|
15
|
+
from simple_module_core.discovery import discover_modules, select_auth_provider, topological_sort
|
|
16
16
|
from simple_module_core.events import EventBus
|
|
17
17
|
from simple_module_core.feature_flags import FeatureFlagRegistry
|
|
18
18
|
from simple_module_core.health import HealthRegistry
|
|
@@ -120,6 +120,13 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
120
120
|
enabled=settings.modules_enabled,
|
|
121
121
|
strict=not settings.is_development,
|
|
122
122
|
)
|
|
123
|
+
# Two auth providers can be installed at once (they are in this workspace);
|
|
124
|
+
# only the configured one is activated. See select_auth_provider / SM020.
|
|
125
|
+
# Strict outside development: diagnostics don't run there, so an
|
|
126
|
+
# unrecognised name would otherwise mount both providers unreported.
|
|
127
|
+
modules = select_auth_provider(
|
|
128
|
+
modules, settings.auth_provider, strict=not settings.is_development
|
|
129
|
+
)
|
|
123
130
|
modules = topological_sort(modules)
|
|
124
131
|
logger.info(
|
|
125
132
|
"Loaded %d module(s): %s",
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/assets.py
RENAMED
|
@@ -13,9 +13,22 @@ A module may ship two optional stylesheets beside its ``pages/`` directory:
|
|
|
13
13
|
utility: a module shipping a bare ``.card { padding: 0 }`` would otherwise
|
|
14
14
|
silently beat ``p-4``.
|
|
15
15
|
|
|
16
|
-
Both are referenced
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
Both are referenced by **absolute** filesystem path, exactly as the ``@source``
|
|
17
|
+
lines in the same file already are.
|
|
18
|
+
|
|
19
|
+
This used to emit a per-module Vite alias (``#module/<pkg>/styles.css``) to
|
|
20
|
+
avoid a relative path like ``../../../.venv/lib/python3.12/site-packages/<pkg>``
|
|
21
|
+
— but that objection only ever applied to *relative* paths, and the alias made
|
|
22
|
+
this generated file depend on the host resolving it. ``vite.config.ts`` is
|
|
23
|
+
scaffold output: it is written once into an app and then owned and edited
|
|
24
|
+
there, so it is versioned independently of these Python packages. A host
|
|
25
|
+
scaffolded before the alias existed never receives it, and a Python-only
|
|
26
|
+
dependency bump would emit specifiers that host cannot resolve, failing the
|
|
27
|
+
build with ``Can't resolve '#module/<pkg>/styles.css'`` — naming something that
|
|
28
|
+
appears nowhere in the app's own sources (GH issue #253).
|
|
29
|
+
|
|
30
|
+
Absolute paths keep the generated CSS self-contained: it resolves under any
|
|
31
|
+
``vite.config.ts``, old or new, with no alias configured at all.
|
|
19
32
|
"""
|
|
20
33
|
|
|
21
34
|
from __future__ import annotations
|
|
@@ -33,7 +46,7 @@ logger = logging.getLogger(__name__)
|
|
|
33
46
|
|
|
34
47
|
THEME_CSS = "theme.css"
|
|
35
48
|
STYLES_CSS = "styles.css"
|
|
36
|
-
|
|
49
|
+
PACKAGE_JSON = "package.json"
|
|
37
50
|
|
|
38
51
|
|
|
39
52
|
@dataclass(frozen=True)
|
|
@@ -46,6 +59,39 @@ class ModuleAssets:
|
|
|
46
59
|
pages_dir: Path | None
|
|
47
60
|
theme_css: Path | None
|
|
48
61
|
styles_css: Path | None
|
|
62
|
+
npm_name: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def find_npm_name(pkg_root: Path) -> str | None:
|
|
66
|
+
"""Return the module's npm package name, or ``None`` if it ships no JS.
|
|
67
|
+
|
|
68
|
+
Two install layouts put ``package.json`` in different places:
|
|
69
|
+
|
|
70
|
+
* **wheel** — Hatch force-includes the module-root ``package.json`` *into*
|
|
71
|
+
the Python package, so it lands at ``site-packages/<pkg>/package.json``.
|
|
72
|
+
* **editable / workspace** — it stays at the source-tree module root,
|
|
73
|
+
``modules/<name>/package.json``, one level above the Python package.
|
|
74
|
+
|
|
75
|
+
The parent candidate is accepted only when that directory also holds a
|
|
76
|
+
``pyproject.toml``. Without that guard a wheel-installed module would
|
|
77
|
+
happily read ``site-packages/package.json`` — some unrelated file that
|
|
78
|
+
happens to sit there — and alias the module onto a stranger's name.
|
|
79
|
+
"""
|
|
80
|
+
candidates = [pkg_root / PACKAGE_JSON]
|
|
81
|
+
parent = pkg_root.parent
|
|
82
|
+
if (parent / "pyproject.toml").is_file():
|
|
83
|
+
candidates.append(parent / PACKAGE_JSON)
|
|
84
|
+
for candidate in candidates:
|
|
85
|
+
if not candidate.is_file():
|
|
86
|
+
continue
|
|
87
|
+
try:
|
|
88
|
+
name = json.loads(candidate.read_text(encoding="utf-8")).get("name")
|
|
89
|
+
except (OSError, ValueError):
|
|
90
|
+
logger.debug("Module package.json at %s is unreadable — ignoring", candidate)
|
|
91
|
+
continue
|
|
92
|
+
if isinstance(name, str) and name:
|
|
93
|
+
return name
|
|
94
|
+
return None
|
|
49
95
|
|
|
50
96
|
|
|
51
97
|
def compute_module_assets(modules: Sequence[ModuleBase]) -> list[ModuleAssets]:
|
|
@@ -78,6 +124,7 @@ def compute_module_assets(modules: Sequence[ModuleBase]) -> list[ModuleAssets]:
|
|
|
78
124
|
pages_dir=pages_dir.resolve() if pages_dir.is_dir() else None,
|
|
79
125
|
theme_css=theme.resolve() if theme.is_file() else None,
|
|
80
126
|
styles_css=styles.resolve() if styles.is_file() else None,
|
|
127
|
+
npm_name=find_npm_name(pkg_root),
|
|
81
128
|
)
|
|
82
129
|
if entry.pages_dir or entry.theme_css or entry.styles_css:
|
|
83
130
|
result.append(entry)
|
|
@@ -109,19 +156,18 @@ def render_modules_css(
|
|
|
109
156
|
``styles.css``, so emitting an absolute one too would just duplicate it.
|
|
110
157
|
``@import`` is emitted for *every* module, in-repo and wheel alike, because
|
|
111
158
|
there is no static-glob equivalent for CSS.
|
|
159
|
+
|
|
160
|
+
Every path is absolute, so nothing here depends on the host's
|
|
161
|
+
``vite.config.ts`` — see the module docstring for why that matters.
|
|
112
162
|
"""
|
|
113
163
|
source_lines = [
|
|
114
164
|
f'@source "{e.pages_dir.as_posix()}/**/*.{{ts,tsx}}";'
|
|
115
165
|
for e in assets
|
|
116
166
|
if e.pages_dir and not in_repo(e.pages_dir)
|
|
117
167
|
]
|
|
118
|
-
theme_lines = [
|
|
119
|
-
f'@import "{ALIAS_PREFIX}/{e.package_name}/{THEME_CSS}";' for e in assets if e.theme_css
|
|
120
|
-
]
|
|
168
|
+
theme_lines = [f'@import "{e.theme_css.as_posix()}";' for e in assets if e.theme_css]
|
|
121
169
|
style_lines = [
|
|
122
|
-
f'@import "{
|
|
123
|
-
for e in assets
|
|
124
|
-
if e.styles_css
|
|
170
|
+
f'@import "{e.styles_css.as_posix()}" layer(components);' for e in assets if e.styles_css
|
|
125
171
|
]
|
|
126
172
|
|
|
127
173
|
out = [_CSS_HEADER]
|
|
@@ -146,6 +192,13 @@ def render_assets_json(assets: Sequence[ModuleAssets]) -> str:
|
|
|
146
192
|
manifest's value shape would break every downstream app at once. This file
|
|
147
193
|
is purely additive, and also covers CSS-only modules that never appear in
|
|
148
194
|
the pages manifest because they ship no ``pages/``.
|
|
195
|
+
|
|
196
|
+
``npm_name`` is what lets one module import another's TS/TSX by package
|
|
197
|
+
name. The host aliases it onto ``package`` — the module's *Python package
|
|
198
|
+
directory* — and that target is forced, not chosen: a wheel contains only
|
|
199
|
+
``site-packages/<pkg>/**``, so the source-tree module root simply does not
|
|
200
|
+
exist once installed. Anchoring the npm name there is the only mapping
|
|
201
|
+
that can mean the same thing in both layouts. See ``find_npm_name``.
|
|
149
202
|
"""
|
|
150
203
|
payload = {
|
|
151
204
|
e.name: {
|
|
@@ -154,6 +207,7 @@ def render_assets_json(assets: Sequence[ModuleAssets]) -> str:
|
|
|
154
207
|
"pages": e.pages_dir.as_posix() if e.pages_dir else None,
|
|
155
208
|
"theme": e.theme_css.as_posix() if e.theme_css else None,
|
|
156
209
|
"styles": e.styles_css.as_posix() if e.styles_css else None,
|
|
210
|
+
"npm_name": e.npm_name,
|
|
157
211
|
}
|
|
158
212
|
for e in assets
|
|
159
213
|
}
|
|
@@ -11,6 +11,7 @@ from typing import Literal
|
|
|
11
11
|
|
|
12
12
|
from pydantic import field_validator, model_validator
|
|
13
13
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
14
|
+
from simple_module_core.discovery import DEFAULT_AUTH_PROVIDER
|
|
14
15
|
from simple_module_core.environments import NON_PROD_ENVIRONMENTS
|
|
15
16
|
|
|
16
17
|
_PLACEHOLDER_SECRET_KEY = "change-me-in-production"
|
|
@@ -46,6 +47,14 @@ class BootstrapSettings(BaseSettings):
|
|
|
46
47
|
|
|
47
48
|
modules_enabled: list[str] | None = None
|
|
48
49
|
|
|
50
|
+
auth_provider: str = DEFAULT_AUTH_PROVIDER
|
|
51
|
+
"""Which auth provider module to activate (``SM_AUTH_PROVIDER``).
|
|
52
|
+
|
|
53
|
+
``users`` and ``keycloak`` both provide authentication and only one may be
|
|
54
|
+
active at a time. When both are installed this picks the winner; the other
|
|
55
|
+
is skipped at discovery. Ignored when only one is installed.
|
|
56
|
+
"""
|
|
57
|
+
|
|
49
58
|
auth_public_paths: list[str] = []
|
|
50
59
|
"""Host-level anonymous-access path prefixes (``SM_AUTH_PUBLIC_PATHS``).
|
|
51
60
|
|
|
@@ -55,6 +64,19 @@ class BootstrapSettings(BaseSettings):
|
|
|
55
64
|
``register_public_routes`` hook, which is method-aware.
|
|
56
65
|
"""
|
|
57
66
|
|
|
67
|
+
@field_validator("auth_provider", mode="after")
|
|
68
|
+
@classmethod
|
|
69
|
+
def _normalize_auth_provider(cls, value: str) -> str:
|
|
70
|
+
"""Strip whitespace; treat blank as unset.
|
|
71
|
+
|
|
72
|
+
``SM_AUTH_PROVIDER=`` in a ``.env`` yields ``''``, which matches no
|
|
73
|
+
installed provider and would mount all of them. The out-of-process
|
|
74
|
+
readers (``make doctor``, ``gen-pages``) go through
|
|
75
|
+
``resolve_auth_provider``, which already falls back on blank — without
|
|
76
|
+
this they and the host would disagree about the active provider.
|
|
77
|
+
"""
|
|
78
|
+
return value.strip() or DEFAULT_AUTH_PROVIDER
|
|
79
|
+
|
|
58
80
|
@field_validator("trusted_proxy", mode="after")
|
|
59
81
|
@classmethod
|
|
60
82
|
def _normalize_trusted_proxy(cls, value: str | None) -> str | None:
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/host_cli.py
RENAMED
|
@@ -13,7 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
from typing import Annotated
|
|
14
14
|
|
|
15
15
|
import typer
|
|
16
|
-
from simple_module_core import discover_modules
|
|
16
|
+
from simple_module_core import discover_modules, resolve_auth_provider, select_auth_provider
|
|
17
17
|
|
|
18
18
|
from simple_module_hosting.manifest import (
|
|
19
19
|
collect_module_js_deps,
|
|
@@ -42,7 +42,9 @@ def gen_pages(
|
|
|
42
42
|
if not host_dir.is_dir():
|
|
43
43
|
typer.echo(f"ERROR: client_app directory not found at {host_dir}", err=True)
|
|
44
44
|
raise typer.Exit(code=1)
|
|
45
|
-
|
|
45
|
+
# Skip the auth provider the host won't boot — its pages would otherwise
|
|
46
|
+
# ship in the bundle with no view endpoint able to render them.
|
|
47
|
+
modules = select_auth_provider(discover_modules(), resolve_auth_provider())
|
|
46
48
|
written = write_module_pages_manifest(modules, host_dir)
|
|
47
49
|
typer.echo(
|
|
48
50
|
f"Module pages manifest: {len(modules)} module(s) "
|
|
@@ -70,7 +72,7 @@ def sync_js_deps(
|
|
|
70
72
|
typer.echo(f"ERROR: client_app directory not found at {host_client_app}", err=True)
|
|
71
73
|
raise typer.Exit(code=1)
|
|
72
74
|
|
|
73
|
-
modules = discover_modules()
|
|
75
|
+
modules = select_auth_provider(discover_modules(), resolve_auth_provider())
|
|
74
76
|
by_module = collect_module_js_deps(modules)
|
|
75
77
|
if not by_module:
|
|
76
78
|
typer.echo("No module JS dependencies declared.")
|
|
@@ -168,16 +168,16 @@ class TestCreateApp:
|
|
|
168
168
|
(tmp_path / "host" / "templates").mkdir(parents=True)
|
|
169
169
|
(tmp_path / "host" / "templates" / "index.html").write_text("<html></html>")
|
|
170
170
|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
app = create_app(Settings(modules_enabled=all_names))
|
|
171
|
+
# No modules_enabled allowlist: both auth providers are entry points in
|
|
172
|
+
# the dev workspace, and create_app is expected to activate the named
|
|
173
|
+
# one rather than fail the boot on SM020. auth_provider is passed
|
|
174
|
+
# explicitly so an SM_AUTH_PROVIDER in the developer's .env can't
|
|
175
|
+
# decide which provider this asserts on.
|
|
176
|
+
app = create_app(Settings(auth_provider="users"))
|
|
178
177
|
|
|
179
178
|
sm = app.state.sm
|
|
180
179
|
assert isinstance(sm, Services)
|
|
180
|
+
assert [m.meta.name for m in sm.modules].count("Keycloak") == 0
|
|
181
181
|
assert sm.settings is not None
|
|
182
182
|
assert sm.db is not None
|
|
183
183
|
assert sm.event_bus is not None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""``SM_AUTH_PROVIDER`` normalisation on BootstrapSettings.
|
|
2
|
+
|
|
3
|
+
The host reads this through ``Settings``; ``make doctor`` and ``smpy host
|
|
4
|
+
gen-pages`` read it through ``simple_module_core.resolve_auth_provider``.
|
|
5
|
+
The two must agree on every input, or the tools report on a different module
|
|
6
|
+
set than the app boots with.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from simple_module_core.discovery import DEFAULT_AUTH_PROVIDER, resolve_auth_provider
|
|
15
|
+
from simple_module_hosting.settings import Settings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _settings(**overrides) -> Settings:
|
|
19
|
+
return Settings(
|
|
20
|
+
database_url="sqlite+aiosqlite:///:memory:",
|
|
21
|
+
environment="testing",
|
|
22
|
+
secret_key="test-secret-key",
|
|
23
|
+
**overrides,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestAuthProviderSetting:
|
|
28
|
+
@pytest.fixture(autouse=True)
|
|
29
|
+
def _isolated_env(self, monkeypatch, tmp_path):
|
|
30
|
+
"""Run against an empty ``.env`` and a throwaway environment.
|
|
31
|
+
|
|
32
|
+
``Settings`` resolves ``env_file=".env"`` relative to the working
|
|
33
|
+
directory, so without the chdir a developer running Keycloak locally
|
|
34
|
+
would fail the default-value assertions below — the very coupling
|
|
35
|
+
these tests exist to pin down.
|
|
36
|
+
"""
|
|
37
|
+
monkeypatch.setattr(os, "environ", dict(os.environ))
|
|
38
|
+
monkeypatch.delenv("SM_AUTH_PROVIDER", raising=False)
|
|
39
|
+
monkeypatch.chdir(tmp_path)
|
|
40
|
+
|
|
41
|
+
def test_defaults_to_users(self):
|
|
42
|
+
assert _settings().auth_provider == DEFAULT_AUTH_PROVIDER
|
|
43
|
+
|
|
44
|
+
def test_explicit_value_passes_through(self):
|
|
45
|
+
assert _settings(auth_provider="keycloak").auth_provider == "keycloak"
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize("raw", ["", " ", "\t"])
|
|
48
|
+
def test_blank_falls_back_to_the_default(self, raw: str):
|
|
49
|
+
"""``SM_AUTH_PROVIDER=`` in .env yielded '', which matches no provider."""
|
|
50
|
+
assert _settings(auth_provider=raw).auth_provider == DEFAULT_AUTH_PROVIDER
|
|
51
|
+
|
|
52
|
+
def test_surrounding_whitespace_stripped(self):
|
|
53
|
+
assert _settings(auth_provider=" keycloak ").auth_provider == "keycloak"
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("raw", ["", " ", "keycloak", " keycloak "])
|
|
56
|
+
def test_agrees_with_resolve_auth_provider(self, raw: str, monkeypatch):
|
|
57
|
+
"""Host and out-of-process readers must land on the same name."""
|
|
58
|
+
monkeypatch.setenv("SM_AUTH_PROVIDER", raw)
|
|
59
|
+
assert _settings().auth_provider == resolve_auth_provider()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/__init__.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/_hydrate_step.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/health.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/host_settings.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/i18n_deps.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/i18n_manifest.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/inertia_deps.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/inertia_utils.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/logging.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/manifest.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/middleware.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/migrations.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/permissions.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/py.typed
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/redirects.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/settings.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/shared_props.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/simple_module_hosting/static_files.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_check_migrations.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_design_packs_wiring.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_error_page_shared_props.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_hosting_permissions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_inertia_manifest.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_inertia_shared_providers.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_locale_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_menu_urls_are_canonical.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_middleware_order.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_precompressed_static.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_response_compression.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_session_cookie_security.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_settings_secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_strict_discovery_wiring.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.28 → simple_module_hosting-0.0.30}/tests/test_tenant_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|