simple-module-core 0.0.27__tar.gz → 0.0.28__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_core-0.0.27 → simple_module_core-0.0.28}/PKG-INFO +1 -1
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/pyproject.toml +1 -1
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/i18n.py +28 -9
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/module.py +9 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_i18n.py +23 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/.gitignore +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/LICENSE +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/README.md +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/__init__.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/__main__.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/design_packs.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/__init__.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_coupling.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_css.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_i18n.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_inertia_api.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_js_workspace.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_migration.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_module.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_pages.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_runner.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_types.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/discovery.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/dotenv.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/environments.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/events.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/exceptions.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/feature_flags.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/health.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/menu.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/permissions.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/public_routes.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/py.typed +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/services.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/versioning.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_css_diagnostics.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_css_scanner.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_design_packs.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_diagnostics.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_discovery.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_dotenv.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_environments.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_events.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_feature_flags.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_feature_flags_decorator.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_health_registry.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_i18n_diagnostics.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_menu.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_module_base.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_module_diagnostics.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_permissions.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_public_routes.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_services.py +0 -0
- {simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_versioning.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_core
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.28
|
|
4
4
|
Summary: Module-system primitives for the simple_module framework — ModuleBase, discovery, diagnostics, events
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -78,7 +78,7 @@ class I18nRegistry:
|
|
|
78
78
|
def __init__(self, default_locale: str, supported_locales: list[str]) -> None:
|
|
79
79
|
self.default_locale = default_locale
|
|
80
80
|
self.supported_locales = list(supported_locales)
|
|
81
|
-
self._sources: list[tuple[str, Path]] = []
|
|
81
|
+
self._sources: list[tuple[str, Path, str]] = []
|
|
82
82
|
self._messages: dict[str, dict[str, str]] = {}
|
|
83
83
|
# Immutable views into ``_messages`` — handed out by ``messages()`` to
|
|
84
84
|
# avoid a per-call dict copy. Rebuilt whenever ``load()`` runs.
|
|
@@ -88,15 +88,22 @@ class I18nRegistry:
|
|
|
88
88
|
# avoids per-request dict copies that used to dominate allocations on the
|
|
89
89
|
# Inertia render path.
|
|
90
90
|
self._message_snapshots: dict[str, dict[str, str]] = {}
|
|
91
|
+
self._public_snapshots: dict[str, dict[str, str]] = {}
|
|
91
92
|
self._available_locales: tuple[str, ...] = ()
|
|
92
93
|
self._available_locales_list: list[str] = []
|
|
93
94
|
self._empty_view: MappingProxyType[str, str] = MappingProxyType({})
|
|
94
95
|
self._empty_snapshot: dict[str, str] = {}
|
|
95
96
|
self._loaded = False
|
|
96
97
|
|
|
97
|
-
def add_source(self, namespace: str, locale_dir: Path) -> None:
|
|
98
|
-
"""Queue a module's locale directory for loading under a namespace.
|
|
99
|
-
|
|
98
|
+
def add_source(self, namespace: str, locale_dir: Path, *, audience: str = "public") -> None:
|
|
99
|
+
"""Queue a module's locale directory for loading under a namespace.
|
|
100
|
+
|
|
101
|
+
``audience="admin"`` keeps the namespace out of the public snapshot
|
|
102
|
+
(:meth:`messages_snapshot` with ``include_admin=False``) so catalogs
|
|
103
|
+
for login-gated UI aren't shipped to anonymous visitors. Server-side
|
|
104
|
+
lookups (:meth:`messages`) always see every namespace.
|
|
105
|
+
"""
|
|
106
|
+
self._sources.append((namespace, Path(locale_dir), audience))
|
|
100
107
|
|
|
101
108
|
def load(self) -> None:
|
|
102
109
|
"""Read and flatten all registered JSON files.
|
|
@@ -105,8 +112,11 @@ class I18nRegistry:
|
|
|
105
112
|
warning but do not raise. Malformed JSON raises ValueError.
|
|
106
113
|
"""
|
|
107
114
|
self._messages = {locale: {} for locale in self.supported_locales}
|
|
115
|
+
public_messages: dict[str, dict[str, str]] = {
|
|
116
|
+
locale: {} for locale in self.supported_locales
|
|
117
|
+
}
|
|
108
118
|
|
|
109
|
-
for namespace, locale_dir in self._sources:
|
|
119
|
+
for namespace, locale_dir, audience in self._sources:
|
|
110
120
|
for locale in self.supported_locales:
|
|
111
121
|
path = locale_dir / f"{locale}.json"
|
|
112
122
|
if not path.is_file():
|
|
@@ -124,6 +134,8 @@ class I18nRegistry:
|
|
|
124
134
|
raise ValueError(f"{path} must contain a JSON object at the top level")
|
|
125
135
|
flat = flatten_messages(raw, prefix=namespace)
|
|
126
136
|
self._messages[locale].update(flat)
|
|
137
|
+
if audience != "admin":
|
|
138
|
+
public_messages[locale].update(flat)
|
|
127
139
|
|
|
128
140
|
# Cache the derived views now that loading is complete. Downstream
|
|
129
141
|
# (middleware, translator, switcher) reads these on every request.
|
|
@@ -131,8 +143,10 @@ class I18nRegistry:
|
|
|
131
143
|
locale: MappingProxyType(msgs) for locale, msgs in self._messages.items()
|
|
132
144
|
}
|
|
133
145
|
# Plain-dict snapshots for serialization callers. ``dict(msgs)`` runs
|
|
134
|
-
# once here rather than on every Inertia render.
|
|
146
|
+
# once here rather than on every Inertia render. The public variant
|
|
147
|
+
# (admin namespaces excluded) is what anonymous visitors receive.
|
|
135
148
|
self._message_snapshots = {locale: dict(msgs) for locale, msgs in self._messages.items()}
|
|
149
|
+
self._public_snapshots = public_messages
|
|
136
150
|
self._available_locales = tuple(locale for locale, msgs in self._messages.items() if msgs)
|
|
137
151
|
self._available_locales_list = list(self._available_locales)
|
|
138
152
|
self._loaded = True
|
|
@@ -165,7 +179,7 @@ class I18nRegistry:
|
|
|
165
179
|
return self._empty_view
|
|
166
180
|
return MappingProxyType(raw)
|
|
167
181
|
|
|
168
|
-
def messages_snapshot(self, locale: str) -> dict[str, str]:
|
|
182
|
+
def messages_snapshot(self, locale: str, *, include_admin: bool = True) -> dict[str, str]:
|
|
169
183
|
"""Plain-dict snapshot for callers that JSON-serialize the result.
|
|
170
184
|
|
|
171
185
|
Built once at :meth:`load` time and handed out by reference on every
|
|
@@ -173,12 +187,17 @@ class I18nRegistry:
|
|
|
173
187
|
corrupts subsequent responses. Used by the Inertia shared-props builder
|
|
174
188
|
where it sits on the request hot path; prior to this method,
|
|
175
189
|
``dict(messages(locale))`` per request was the top own-code allocator.
|
|
190
|
+
|
|
191
|
+
``include_admin=False`` returns the variant without ``audience="admin"``
|
|
192
|
+
namespaces — what anonymous visitors are served.
|
|
176
193
|
"""
|
|
177
|
-
|
|
194
|
+
pool = self._message_snapshots if include_admin else self._public_snapshots
|
|
195
|
+
snapshot = pool.get(locale)
|
|
178
196
|
if snapshot is not None:
|
|
179
197
|
return snapshot
|
|
180
198
|
# Fallback for tests that skip ``load()``: synthesize the snapshot on
|
|
181
|
-
# demand from whatever ``_messages`` holds
|
|
199
|
+
# demand from whatever ``_messages`` holds (audience information only
|
|
200
|
+
# exists for sources that went through ``load()``).
|
|
182
201
|
raw = self._messages.get(locale)
|
|
183
202
|
if raw is None:
|
|
184
203
|
return self._empty_snapshot
|
|
@@ -35,6 +35,15 @@ class ModuleMeta:
|
|
|
35
35
|
installed ``simple_module_core.FRAMEWORK_API_VERSION`` does not satisfy it.
|
|
36
36
|
When ``None``, no compatibility check is performed (legacy modules).
|
|
37
37
|
"""
|
|
38
|
+
i18n_audience: str = "public"
|
|
39
|
+
"""Who this module's locale catalog is shipped to: ``"public"`` or ``"admin"``.
|
|
40
|
+
|
|
41
|
+
``"public"`` (the default) ships the catalog in every Inertia payload.
|
|
42
|
+
``"admin"`` ships it only to authenticated users — declare it on modules
|
|
43
|
+
whose UI sits entirely behind login (settings, permissions, dashboards) so
|
|
44
|
+
anonymous visitors don't download admin form labels on every public page.
|
|
45
|
+
The catalog is always available server-side (``Translator``) either way.
|
|
46
|
+
"""
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
class ModuleBase(ABC):
|
|
@@ -65,6 +65,29 @@ class TestI18nRegistry:
|
|
|
65
65
|
reg.load()
|
|
66
66
|
assert sorted(reg.available_locales()) == ["en", "es"]
|
|
67
67
|
|
|
68
|
+
def test_admin_sources_are_excluded_from_the_public_snapshot(self, tmp_path: Path) -> None:
|
|
69
|
+
"""Anonymous visitors must not download catalogs for login-gated UI."""
|
|
70
|
+
self._write_locale(tmp_path / "pages", "en", {"title": "Pages"})
|
|
71
|
+
self._write_locale(tmp_path / "settings", "en", {"form": {"key": "Key"}})
|
|
72
|
+
reg = I18nRegistry(default_locale="en", supported_locales=["en"])
|
|
73
|
+
reg.add_source("pages", tmp_path / "pages")
|
|
74
|
+
reg.add_source("settings", tmp_path / "settings", audience="admin")
|
|
75
|
+
reg.load()
|
|
76
|
+
assert reg.messages_snapshot("en", include_admin=False) == {"pages.title": "Pages"}
|
|
77
|
+
# The full snapshot and server-side lookups still see everything.
|
|
78
|
+
assert reg.messages_snapshot("en") == {
|
|
79
|
+
"pages.title": "Pages",
|
|
80
|
+
"settings.form.key": "Key",
|
|
81
|
+
}
|
|
82
|
+
assert reg.messages("en")["settings.form.key"] == "Key"
|
|
83
|
+
|
|
84
|
+
def test_public_snapshot_defaults_to_everything(self, tmp_path: Path) -> None:
|
|
85
|
+
self._write_locale(tmp_path / "p", "en", {"title": "Products"})
|
|
86
|
+
reg = I18nRegistry(default_locale="en", supported_locales=["en"])
|
|
87
|
+
reg.add_source("products", tmp_path / "p")
|
|
88
|
+
reg.load()
|
|
89
|
+
assert reg.messages_snapshot("en", include_admin=False) == reg.messages_snapshot("en")
|
|
90
|
+
|
|
68
91
|
def test_missing_locale_file_is_warning_not_error(
|
|
69
92
|
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
70
93
|
) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/__init__.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_coupling.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_css.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_i18n.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_migration.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_module.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_pages.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_runner.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/simple_module_core/diagnostics/_types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.27 → simple_module_core-0.0.28}/tests/test_feature_flags_decorator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|