simple-module-core 0.0.41__tar.gz → 0.0.42__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.41 → simple_module_core-0.0.42}/PKG-INFO +2 -2
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/pyproject.toml +2 -2
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/__init__.py +2 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/__main__.py +8 -1
- simple_module_core-0.0.42/simple_module_core/_data_doctor.py +22 -0
- simple_module_core-0.0.42/simple_module_core/data_compatibility.py +43 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/durable_events.py +11 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/events.py +7 -1
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/feature_flags.py +6 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/module.py +11 -1
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/resources.py +44 -3
- simple_module_core-0.0.42/tests/test_core_data_compatibility_doctor.py +45 -0
- simple_module_core-0.0.42/tests/test_core_inline_delivery_context.py +19 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/.gitignore +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/LICENSE +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/README.md +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/_i18n_flatten.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/_i18n_translator.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/_permission_grants.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/_request_hooks.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/activity_feed.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/app_state.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/audit_links.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/body_limits.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/correlation.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/cors.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/csp.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/datetimes.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/deprecation.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/design_packs.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/__init__.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_coupling.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_css.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_db_extensions.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_dependencies.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_expression_index.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_heavy_task_imports.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_i18n.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_inertia_api.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_js_workspace.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_migration.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_module.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_pages.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_runner.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_spa_mount.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_tenancy.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_types.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/discovery.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/dotenv.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/environments.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/exceptions.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/health.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/i18n.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/i18n_overrides.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/invalidation.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/invalidation_refresh.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/leases.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/mail.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/menu.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/module_meta.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/module_settings.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/path_rules.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/permission_denied.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/permissions.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/public_routes.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/py.typed +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/rate_limit.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/redirect_safety.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/secret_fields.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/services.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/settings_base.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/settings_hydrate.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/setup_steps.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/spa_mount.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/stored_settings.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/tenancy.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/tenant_events.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/user_directory.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/user_events.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/versioning.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_audit_links.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_auth_provider_selection.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_app_state.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_datetimes.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_invalidation_refresh.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_leases.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_lifecycle_docs.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_mail_resolution.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_module_settings.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_user_directory.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_csp_registry.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_css_diagnostics.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_css_scanner.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_db_extensions_diagnostics.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_deprecation_helper.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_design_packs.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_diagnostics.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_diagnostics_tenancy.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_discovery.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_discovery_missing_deps.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_doctor_cli_overrides.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_dotenv.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_durable_events.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_environments.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_events.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_events_inline.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_expression_index_diagnostics.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_feature_flags.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_feature_flags_decorator.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_health_registry.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_heavy_task_imports_diagnostics.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_i18n.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_i18n_diagnostics.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_i18n_overrides.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_invalidation.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_invalidation_handlers.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_menu.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_module_base.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_module_diagnostics.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_path_rule_registries.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_permission_sources.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_permissions.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_permissions_admin_principal.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_public_routes.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_public_routes_rate_precedence.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_redirect_safety.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_services.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_settings_base.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_sm003_page_render.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_sm009_dynamic_coupling.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_tenant_exists.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_tenant_roles.py +0 -0
- {simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_versioning.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_core
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.42
|
|
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
|
|
@@ -22,7 +22,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
24
|
Requires-Dist: babel<3,>=2.14
|
|
25
|
-
Requires-Dist: fastapi<0.
|
|
25
|
+
Requires-Dist: fastapi<0.160,>=0.140
|
|
26
26
|
Requires-Dist: packaging<27,>=23.0
|
|
27
27
|
Requires-Dist: pydantic-settings<3,>=2.0
|
|
28
28
|
Requires-Dist: pydantic<3,>=2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_core"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.42"
|
|
4
4
|
description = "Module-system primitives for the simple_module framework — ModuleBase, discovery, diagnostics, events"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
24
|
"babel>=2.14,<3",
|
|
25
|
-
"fastapi>=0.140,<0.
|
|
25
|
+
"fastapi>=0.140,<0.160",
|
|
26
26
|
"packaging>=23.0,<27",
|
|
27
27
|
"pydantic>=2.0,<3",
|
|
28
28
|
"pydantic-settings>=2.0,<3",
|
|
@@ -4,6 +4,7 @@ from simple_module_core.audit_links import AuditLink, AuditLinkRegistry, LabelRe
|
|
|
4
4
|
from simple_module_core.body_limits import BodyLimitRegistry
|
|
5
5
|
from simple_module_core.cors import CorsPolicy, CorsPolicyRegistry
|
|
6
6
|
from simple_module_core.csp import CspSourceError, CspSourceRegistry
|
|
7
|
+
from simple_module_core.data_compatibility import DataCompatibilityIssue
|
|
7
8
|
from simple_module_core.design_packs import DesignPack, DesignPackRegistry
|
|
8
9
|
from simple_module_core.diagnostics import (
|
|
9
10
|
DiagnosticLevel,
|
|
@@ -78,6 +79,7 @@ __all__ = [
|
|
|
78
79
|
"CorsPolicyRegistry",
|
|
79
80
|
"CspSourceError",
|
|
80
81
|
"CspSourceRegistry",
|
|
82
|
+
"DataCompatibilityIssue",
|
|
81
83
|
"DesignPack",
|
|
82
84
|
"DesignPackRegistry",
|
|
83
85
|
"DiagnosticLevel",
|
|
@@ -24,6 +24,7 @@ import os
|
|
|
24
24
|
import sys
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
|
+
from simple_module_core._data_doctor import data_compatibility_diagnostics
|
|
27
28
|
from simple_module_core.diagnostics import (
|
|
28
29
|
Diagnostic,
|
|
29
30
|
DiagnosticLevel,
|
|
@@ -75,13 +76,18 @@ def _load_i18n_settings_from_env() -> tuple[list[str] | None, str]:
|
|
|
75
76
|
return supported, default
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
def _database_url() -> str:
|
|
80
|
+
"""``SM_DATABASE_URL``, defaulting like ``Settings.database_url``."""
|
|
81
|
+
return os.environ.get("SM_DATABASE_URL", "sqlite+aiosqlite:///./app.db")
|
|
82
|
+
|
|
83
|
+
|
|
78
84
|
def _database_dialect() -> str:
|
|
79
85
|
"""Dialect name of ``SM_DATABASE_URL`` (``.env`` merged by the i18n loader first).
|
|
80
86
|
|
|
81
87
|
Defaults to ``sqlite`` like ``Settings.database_url``. Reads the env var
|
|
82
88
|
directly for the same reason the i18n settings do: no hosting import.
|
|
83
89
|
"""
|
|
84
|
-
url =
|
|
90
|
+
url = _database_url()
|
|
85
91
|
return url.split(":", 1)[0].split("+", 1)[0]
|
|
86
92
|
|
|
87
93
|
|
|
@@ -182,6 +188,7 @@ def main() -> int:
|
|
|
182
188
|
database_dialect=_database_dialect(),
|
|
183
189
|
)
|
|
184
190
|
diagnostics.extend(override_findings)
|
|
191
|
+
diagnostics.extend(data_compatibility_diagnostics(modules, _database_url()))
|
|
185
192
|
# A missing dependency only warns at a dev boot, but production refuses
|
|
186
193
|
# to start with it (GH #501), so doctor reports it as what it will be.
|
|
187
194
|
for finding in diagnostics:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Optional database diagnostics for Doctor without requiring a host app import."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib.util
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from simple_module_core.data_compatibility import checking_modules
|
|
8
|
+
from simple_module_core.diagnostics import Diagnostic
|
|
9
|
+
from simple_module_core.module import ModuleBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def data_compatibility_diagnostics(
|
|
13
|
+
modules: Sequence[ModuleBase], database_url: str
|
|
14
|
+
) -> list[Diagnostic]:
|
|
15
|
+
# Core-only installations have no database runtime to inspect.
|
|
16
|
+
if importlib.util.find_spec("simple_module_db") is None:
|
|
17
|
+
return []
|
|
18
|
+
if not checking_modules(modules):
|
|
19
|
+
return []
|
|
20
|
+
from simple_module_db.data_compatibility import database_compatibility_diagnostics
|
|
21
|
+
|
|
22
|
+
return asyncio.run(database_compatibility_diagnostics(modules, database_url))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Semantic, read-only checks for upgrades in host-owned migration histories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from simple_module_core.module import ModuleBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, kw_only=True)
|
|
14
|
+
class DataCompatibilityIssue:
|
|
15
|
+
"""An outstanding module upgrade, independent of Alembic revision ids."""
|
|
16
|
+
|
|
17
|
+
upgrade_id: str
|
|
18
|
+
message: str
|
|
19
|
+
severity: Literal["warning", "error"]
|
|
20
|
+
|
|
21
|
+
def remediation(self) -> str:
|
|
22
|
+
return f"smpy migrations prepare {self.upgrade_id} --head <host-head-or-label@head>"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def collect_data_compatibility(
|
|
26
|
+
modules: Sequence[ModuleBase], db: Any
|
|
27
|
+
) -> list[DataCompatibilityIssue]:
|
|
28
|
+
"""Read module state without applying migrations or changing credentials."""
|
|
29
|
+
issues = []
|
|
30
|
+
for module in checking_modules(modules):
|
|
31
|
+
issues.extend(await module.check_data_compatibility(db))
|
|
32
|
+
return issues
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def checking_modules(modules: Sequence[ModuleBase]) -> list[ModuleBase]:
|
|
36
|
+
"""Modules that override the hook; the rest need no database session."""
|
|
37
|
+
from simple_module_core.module import ModuleBase
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
module
|
|
41
|
+
for module in modules
|
|
42
|
+
if type(module).check_data_compatibility is not ModuleBase.check_data_compatibility
|
|
43
|
+
]
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/durable_events.py
RENAMED
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
23
|
from collections.abc import Awaitable, Callable, Sequence
|
|
24
|
+
from contextvars import ContextVar
|
|
24
25
|
from dataclasses import dataclass
|
|
25
26
|
from functools import cache
|
|
26
27
|
from typing import Any, Protocol, runtime_checkable
|
|
@@ -33,6 +34,14 @@ logger = logging.getLogger(__name__)
|
|
|
33
34
|
# subscribe time so an over-long name fails at boot, not on a publisher's flush.
|
|
34
35
|
MAX_NAME_LENGTH = 255
|
|
35
36
|
|
|
37
|
+
# Set by delivery transports only while invoking a durable handler.
|
|
38
|
+
IN_DELIVERY: ContextVar[bool] = ContextVar("durable_event_in_delivery", default=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def in_delivery() -> bool:
|
|
42
|
+
"""Whether this context runs in a durable handler (including child tasks)."""
|
|
43
|
+
return IN_DELIVERY.get()
|
|
44
|
+
|
|
36
45
|
|
|
37
46
|
class DurableDeliveryUnavailable(RuntimeError): # noqa: N818 - name fixed by the design
|
|
38
47
|
"""``publish_durable`` was called but no durable transport is installed.
|
|
@@ -177,6 +186,7 @@ class DurableRegistry:
|
|
|
177
186
|
|
|
178
187
|
|
|
179
188
|
__all__ = [
|
|
189
|
+
"IN_DELIVERY",
|
|
180
190
|
"DurableContext",
|
|
181
191
|
"DurableDeliveryUnavailable",
|
|
182
192
|
"DurableHandler",
|
|
@@ -186,5 +196,6 @@ __all__ = [
|
|
|
186
196
|
"deserialize_event",
|
|
187
197
|
"event_key",
|
|
188
198
|
"handler_name",
|
|
199
|
+
"in_delivery",
|
|
189
200
|
"serialize_event",
|
|
190
201
|
]
|
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
from pyee.asyncio import AsyncIOEventEmitter
|
|
12
12
|
|
|
13
13
|
from simple_module_core.durable_events import (
|
|
14
|
+
IN_DELIVERY,
|
|
14
15
|
DurableContext,
|
|
15
16
|
DurableDeliveryUnavailable,
|
|
16
17
|
DurableHandler,
|
|
@@ -160,4 +161,9 @@ class EventBus:
|
|
|
160
161
|
subscription = self.durable.lookup(key, name)
|
|
161
162
|
if subscription is None:
|
|
162
163
|
continue
|
|
163
|
-
|
|
164
|
+
# Inline delivery is still a durable delivery for ``in_delivery()`` (#682).
|
|
165
|
+
token = IN_DELIVERY.set(True)
|
|
166
|
+
try:
|
|
167
|
+
await subscription.handler(event, ctx)
|
|
168
|
+
finally:
|
|
169
|
+
IN_DELIVERY.reset(token)
|
|
@@ -71,6 +71,12 @@ class FeatureFlagRegistry:
|
|
|
71
71
|
else:
|
|
72
72
|
self._tenant_overrides.pop((name, tenant_id), None)
|
|
73
73
|
|
|
74
|
+
def clear_tenant_overrides(self, tenant_id: str) -> None:
|
|
75
|
+
"""Forget every override for a deleted tenant, including unknown flags."""
|
|
76
|
+
self._tenant_overrides = {
|
|
77
|
+
key: value for key, value in self._tenant_overrides.items() if key[1] != tenant_id
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
def replace_overrides(
|
|
75
81
|
self, system: dict[str, bool], tenants: dict[tuple[str, str], bool]
|
|
76
82
|
) -> None:
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from abc import ABC
|
|
6
|
+
from collections.abc import Sequence
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
8
9
|
|
|
9
10
|
from simple_module_core._request_hooks import RequestHooksMixin
|
|
11
|
+
from simple_module_core.data_compatibility import DataCompatibilityIssue
|
|
10
12
|
from simple_module_core.module_meta import ModuleMeta
|
|
11
13
|
|
|
12
14
|
if TYPE_CHECKING:
|
|
@@ -226,6 +228,14 @@ class ModuleBase(RequestHooksMixin, ABC):
|
|
|
226
228
|
|
|
227
229
|
# ── Lifecycle ─────────────────────────────────────────────
|
|
228
230
|
|
|
231
|
+
async def check_data_compatibility(self, db: Any) -> Sequence[DataCompatibilityIssue]:
|
|
232
|
+
"""Read-only upgrade checks before settings hydration and module startup.
|
|
233
|
+
|
|
234
|
+
Report semantic state, never demand an upstream Alembic revision id:
|
|
235
|
+
scaffolded hosts own their histories and may port an equivalent step.
|
|
236
|
+
"""
|
|
237
|
+
return ()
|
|
238
|
+
|
|
229
239
|
async def on_startup(self, app: FastAPI) -> None:
|
|
230
240
|
"""Called after all modules are registered, during app startup."""
|
|
231
241
|
|
|
@@ -25,10 +25,11 @@ typed ``Any``; the stock policy lives in ``simple_module_db.resource_policy``.
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
27
|
import re
|
|
28
|
-
from collections.abc import Callable
|
|
28
|
+
from collections.abc import Callable, Mapping
|
|
29
29
|
from dataclasses import dataclass, field
|
|
30
30
|
from typing import Any, Protocol, runtime_checkable
|
|
31
31
|
|
|
32
|
+
from simple_module_core.deprecation import deprecate
|
|
32
33
|
from simple_module_core.permissions import grants
|
|
33
34
|
|
|
34
35
|
ACTION_VIEW = "view"
|
|
@@ -39,7 +40,13 @@ ACTION_DELETE = "delete"
|
|
|
39
40
|
ACTION_SHARE = "share"
|
|
40
41
|
"""Granting someone else access; the ``sharing`` module checks it on the granter."""
|
|
41
42
|
|
|
43
|
+
_SHAREABLE_ACTIONS = deprecate(
|
|
44
|
+
"ResourceType.shareable_actions", remove_in="0.1.0", use="ResourceType.share_roles"
|
|
45
|
+
)
|
|
46
|
+
|
|
42
47
|
_TYPE_NAME = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$")
|
|
48
|
+
# A share role id is stored in a 32-character column (``sharing`` ``ROLE_MAX``).
|
|
49
|
+
_ROLE_ID = re.compile(r"^[a-z][a-z0-9_]{0,31}$")
|
|
43
50
|
|
|
44
51
|
|
|
45
52
|
@dataclass(frozen=True)
|
|
@@ -87,13 +94,36 @@ class ResourceType:
|
|
|
87
94
|
``name`` is dotted and owned by the module (``"notes.note"``). ``select_by_id``
|
|
88
95
|
builds the unfiltered ``SELECT`` for one row from its string id; callers
|
|
89
96
|
always pass it through ``policy.filter`` before executing it.
|
|
97
|
+
|
|
98
|
+
``share_roles`` maps each share role id this type allows to the actions it
|
|
99
|
+
implies (each must include ``view``; checked at registration). ``None``
|
|
100
|
+
means the ``sharing`` module's default view/edit roles. Pass the same
|
|
101
|
+
mapping to ``sharing``'s ``share_exists_for(..., roles=...)``.
|
|
102
|
+
``shareable_actions`` is deprecated in favour of ``share_roles``.
|
|
90
103
|
"""
|
|
91
104
|
|
|
92
105
|
name: str
|
|
93
106
|
policy: ResourcePolicy
|
|
94
107
|
select_by_id: StatementForId
|
|
95
|
-
#:
|
|
96
|
-
shareable_actions: tuple[str, ...]
|
|
108
|
+
#: Deprecated role allowlist; retained for view/edit subsets until 0.1.0.
|
|
109
|
+
shareable_actions: tuple[str, ...] | None = None
|
|
110
|
+
#: Role ids mapped to their implied actions; None uses sharing's view/edit default.
|
|
111
|
+
share_roles: Mapping[str, tuple[str, ...]] | None = field(
|
|
112
|
+
default=None, kw_only=True, hash=False
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def __post_init__(self) -> None:
|
|
116
|
+
if self.shareable_actions is not None:
|
|
117
|
+
_SHAREABLE_ACTIONS.warn(stacklevel=3)
|
|
118
|
+
if self.share_roles is None:
|
|
119
|
+
object.__setattr__(
|
|
120
|
+
self,
|
|
121
|
+
"share_roles",
|
|
122
|
+
{
|
|
123
|
+
role: (ACTION_VIEW,) if role == ACTION_VIEW else (ACTION_VIEW, role)
|
|
124
|
+
for role in self.shareable_actions
|
|
125
|
+
},
|
|
126
|
+
)
|
|
97
127
|
|
|
98
128
|
|
|
99
129
|
class ResourceTypeError(ValueError):
|
|
@@ -115,6 +145,17 @@ class ResourceTypeRegistry:
|
|
|
115
145
|
raise ResourceTypeError(f"Resource type name {name!r} must look like 'module.thing'")
|
|
116
146
|
if name in self._types:
|
|
117
147
|
raise ResourceTypeError(f"Resource type {name!r} is registered twice")
|
|
148
|
+
if resource_type.share_roles is not None:
|
|
149
|
+
for role, actions in resource_type.share_roles.items():
|
|
150
|
+
if not isinstance(role, str) or not _ROLE_ID.fullmatch(role):
|
|
151
|
+
raise ResourceTypeError(
|
|
152
|
+
f"Resource type {name!r} share role {role!r} must be a lowercase "
|
|
153
|
+
"identifier of at most 32 characters"
|
|
154
|
+
)
|
|
155
|
+
if isinstance(actions, str) or ACTION_VIEW not in actions:
|
|
156
|
+
raise ResourceTypeError(
|
|
157
|
+
f"Resource type {name!r} share role {role!r} must include {ACTION_VIEW!r}"
|
|
158
|
+
)
|
|
118
159
|
self._types[name] = resource_type
|
|
119
160
|
|
|
120
161
|
def get(self, name: str) -> ResourceType | None:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Both Doctor entry points report upgrade ids and return a failing status (#711)."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import sqlalchemy as sa
|
|
5
|
+
from simple_module_core import ModuleBase, ModuleMeta
|
|
6
|
+
from simple_module_core import __main__ as doctor
|
|
7
|
+
from simple_module_core.data_compatibility import DataCompatibilityIssue
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize("entrypoint", ["make", "smpy"])
|
|
11
|
+
def test_core_doctor_reports_semantic_data_errors(tmp_path, monkeypatch, capsys, entrypoint):
|
|
12
|
+
path = tmp_path / "doctor.db"
|
|
13
|
+
engine = sa.create_engine(f"sqlite:///{path}")
|
|
14
|
+
with engine.begin() as conn:
|
|
15
|
+
conn.execute(sa.text("CREATE TABLE alembic_version (version_num VARCHAR(32))"))
|
|
16
|
+
conn.execute(sa.text("INSERT INTO alembic_version VALUES ('local')"))
|
|
17
|
+
engine.dispose()
|
|
18
|
+
|
|
19
|
+
class Module(ModuleBase):
|
|
20
|
+
meta = ModuleMeta(name="Example")
|
|
21
|
+
|
|
22
|
+
async def check_data_compatibility(self, db):
|
|
23
|
+
assert await db.scalar(sa.text("SELECT version_num FROM alembic_version")) == "local"
|
|
24
|
+
return [
|
|
25
|
+
DataCompatibilityIssue(
|
|
26
|
+
upgrade_id="example.v1", message="Upgrade required", severity="error"
|
|
27
|
+
)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
monkeypatch.setenv("SM_DATABASE_URL", f"sqlite+aiosqlite:///{path}")
|
|
31
|
+
monkeypatch.setattr(doctor, "discover_modules", lambda strict: [Module()])
|
|
32
|
+
monkeypatch.setattr(doctor, "parse_dotenv", dict)
|
|
33
|
+
monkeypatch.setattr(doctor, "run_diagnostics", lambda *a, **kw: [])
|
|
34
|
+
if entrypoint == "make":
|
|
35
|
+
assert doctor.main() == 1
|
|
36
|
+
output = capsys.readouterr().err
|
|
37
|
+
else:
|
|
38
|
+
from simple_module_cli.cli import app
|
|
39
|
+
from typer.testing import CliRunner
|
|
40
|
+
|
|
41
|
+
result = CliRunner().invoke(app, ["doctor"])
|
|
42
|
+
assert result.exit_code == 1
|
|
43
|
+
output = result.output
|
|
44
|
+
assert "example.v1" in output
|
|
45
|
+
assert "smpy migrations prepare example.v1" in output
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Inline durable dispatch marks its handlers as in-delivery (#682)."""
|
|
2
|
+
|
|
3
|
+
from simple_module_core.durable_events import in_delivery
|
|
4
|
+
from simple_module_core.events import EventBus
|
|
5
|
+
from simple_module_core.tenant_events import TenantDeleted
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def test_inline_durable_handler_sees_in_delivery():
|
|
9
|
+
bus = EventBus()
|
|
10
|
+
seen: list[bool] = []
|
|
11
|
+
|
|
12
|
+
async def handler(event, ctx):
|
|
13
|
+
seen.append(in_delivery())
|
|
14
|
+
|
|
15
|
+
bus.subscribe_durable(TenantDeleted, handler, name="probe")
|
|
16
|
+
assert in_delivery() is False
|
|
17
|
+
await bus.publish_durable_or_inline(TenantDeleted("id", "slug", "Name"), object())
|
|
18
|
+
assert seen == [True]
|
|
19
|
+
assert in_delivery() is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/_i18n_translator.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/_permission_grants.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/_request_hooks.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
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/__init__.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_coupling.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_css.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_i18n.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_migration.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_module.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_pages.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_runner.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_spa_mount.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/diagnostics/_tenancy.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/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
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/i18n_overrides.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/invalidation_refresh.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/module_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/permission_denied.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/redirect_safety.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/settings_hydrate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/stored_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/simple_module_core/user_directory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_auth_provider_selection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_core_invalidation_refresh.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
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_db_extensions_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_discovery_missing_deps.py
RENAMED
|
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.41 → simple_module_core-0.0.42}/tests/test_expression_index_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_feature_flags_decorator.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_heavy_task_imports_diagnostics.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
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_permissions_admin_principal.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_public_routes_rate_precedence.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.41 → simple_module_core-0.0.42}/tests/test_sm009_dynamic_coupling.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|