simple-module-core 0.0.17__tar.gz → 0.0.18__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.17 → simple_module_core-0.0.18}/PKG-INFO +1 -1
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/pyproject.toml +1 -1
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/__init__.py +3 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_module.py +1 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/module.py +19 -0
- simple_module_core-0.0.18/simple_module_core/public_routes.py +129 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/services.py +2 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_module_base.py +24 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_module_diagnostics.py +30 -0
- simple_module_core-0.0.18/tests/test_public_routes.py +94 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_services.py +3 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/.gitignore +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/LICENSE +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/README.md +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/__main__.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/__init__.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_coupling.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_i18n.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_inertia_api.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_js_workspace.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_migration.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_pages.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_runner.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_types.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/discovery.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/dotenv.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/environments.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/events.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/exceptions.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/feature_flags.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/health.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/i18n.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/menu.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/permissions.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/py.typed +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/versioning.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_diagnostics.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_discovery.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_dotenv.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_environments.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_events.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_feature_flags.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_feature_flags_decorator.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_health_registry.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_i18n.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_i18n_diagnostics.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_menu.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/tests/test_permissions.py +0 -0
- {simple_module_core-0.0.17 → simple_module_core-0.0.18}/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.18
|
|
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
|
|
@@ -33,6 +33,7 @@ from simple_module_core.i18n import I18nRegistry, Translator
|
|
|
33
33
|
from simple_module_core.menu import MenuItem, MenuRegistry, MenuSection
|
|
34
34
|
from simple_module_core.module import ModuleBase, ModuleMeta
|
|
35
35
|
from simple_module_core.permissions import PermissionRegistry
|
|
36
|
+
from simple_module_core.public_routes import PublicRoute, PublicRouteRegistry
|
|
36
37
|
from simple_module_core.services import Services
|
|
37
38
|
from simple_module_core.versioning import FRAMEWORK_API_VERSION, check_framework_compatibility
|
|
38
39
|
|
|
@@ -60,6 +61,8 @@ __all__ = [
|
|
|
60
61
|
"ModuleMeta",
|
|
61
62
|
"NotFoundError",
|
|
62
63
|
"PermissionRegistry",
|
|
64
|
+
"PublicRoute",
|
|
65
|
+
"PublicRouteRegistry",
|
|
63
66
|
"Services",
|
|
64
67
|
"Translator",
|
|
65
68
|
"ValidationError",
|
|
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
|
|
15
15
|
from simple_module_core.health import HealthRegistry
|
|
16
16
|
from simple_module_core.menu import MenuRegistry
|
|
17
17
|
from simple_module_core.permissions import PermissionRegistry
|
|
18
|
+
from simple_module_core.public_routes import PublicRouteRegistry
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@dataclass(frozen=True)
|
|
@@ -118,6 +119,24 @@ class ModuleBase(ABC):
|
|
|
118
119
|
def register_health_checks(self, registry: HealthRegistry) -> None:
|
|
119
120
|
"""Contribute health checks for the ``/health/ready`` endpoint."""
|
|
120
121
|
|
|
122
|
+
def register_public_routes(self, registry: PublicRouteRegistry) -> None:
|
|
123
|
+
"""Declare routes that must bypass authentication (anonymous access).
|
|
124
|
+
|
|
125
|
+
``AuthMiddleware`` gates every request behind the active auth provider.
|
|
126
|
+
Override this hook to exempt read-only or webhook routes that are meant
|
|
127
|
+
to be reached without a session — e.g. a STAC / OGC API surface::
|
|
128
|
+
|
|
129
|
+
def register_public_routes(self, registry):
|
|
130
|
+
registry.add_prefix("/api/gis/stac")
|
|
131
|
+
registry.add_regex(
|
|
132
|
+
r"/api/gis/datasets/[^/]+/tilejson$", methods={"GET"}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
Rules are method-aware, so a GET read route nested under a prefix that
|
|
136
|
+
also carries POST/PATCH mutations can be exempted without opening the
|
|
137
|
+
mutations. Called once at boot, in dependency order.
|
|
138
|
+
"""
|
|
139
|
+
|
|
121
140
|
def register_middleware(self, app: FastAPI) -> None:
|
|
122
141
|
"""Add middleware to the application.
|
|
123
142
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Public-route registry — modules declare routes the auth layer must NOT gate.
|
|
2
|
+
|
|
3
|
+
``AuthMiddleware`` gates every request behind the active auth provider. Modules
|
|
4
|
+
that expose anonymous read APIs (STAC / OGC API / TileJSON, public webhooks,
|
|
5
|
+
status pages) contribute exemptions here via
|
|
6
|
+
:meth:`~simple_module_core.module.ModuleBase.register_public_routes`. The host
|
|
7
|
+
collects them into one registry at boot and the middleware consults it on every
|
|
8
|
+
request.
|
|
9
|
+
|
|
10
|
+
Unlike the legacy ``AuthProvider.get_public_paths`` contract — a flat tuple of
|
|
11
|
+
prefixes matched with ``str.startswith`` — a :class:`PublicRoute` is
|
|
12
|
+
**method-aware** and supports prefix / exact / suffix / regex matching. That
|
|
13
|
+
lets a module expose ``GET /api/gis/datasets/{id}/tilejson`` while leaving
|
|
14
|
+
``PATCH``/``POST`` siblings under the same prefix authenticated.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
|
|
22
|
+
_MatchKind = str # one of: "prefix" | "exact" | "suffix" | "regex"
|
|
23
|
+
_VALID_KINDS = ("prefix", "exact", "suffix", "regex")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PublicRoute:
|
|
27
|
+
"""A single anonymous-access rule.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
pattern: The path (or path fragment / regex) to match against
|
|
31
|
+
``request.url.path``.
|
|
32
|
+
methods: HTTP methods this rule applies to (case-insensitive). ``None``
|
|
33
|
+
(the default) means *any* method — the rule matches every verb.
|
|
34
|
+
kind: How ``pattern`` is interpreted — ``"prefix"`` (default, matches
|
|
35
|
+
any path that starts with it), ``"exact"``, ``"suffix"``, or
|
|
36
|
+
``"regex"`` (anchored at the start of the path via ``re.match``).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ("_regex", "kind", "methods", "pattern")
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
pattern: str,
|
|
44
|
+
*,
|
|
45
|
+
methods: Iterable[str] | None = None,
|
|
46
|
+
kind: _MatchKind = "prefix",
|
|
47
|
+
) -> None:
|
|
48
|
+
if kind not in _VALID_KINDS:
|
|
49
|
+
raise ValueError(f"Unknown match kind {kind!r}; expected one of {_VALID_KINDS}")
|
|
50
|
+
self.pattern = pattern
|
|
51
|
+
self.methods: frozenset[str] | None = (
|
|
52
|
+
None if methods is None else frozenset(m.upper() for m in methods)
|
|
53
|
+
)
|
|
54
|
+
self.kind = kind
|
|
55
|
+
self._regex = re.compile(pattern) if kind == "regex" else None
|
|
56
|
+
|
|
57
|
+
def matches(self, method: str, path: str) -> bool:
|
|
58
|
+
"""Return ``True`` if *method* + *path* are exempt under this rule."""
|
|
59
|
+
if self.methods is not None and method.upper() not in self.methods:
|
|
60
|
+
return False
|
|
61
|
+
if self.kind == "prefix":
|
|
62
|
+
return path.startswith(self.pattern)
|
|
63
|
+
if self.kind == "exact":
|
|
64
|
+
return path == self.pattern
|
|
65
|
+
if self.kind == "suffix":
|
|
66
|
+
return path.endswith(self.pattern)
|
|
67
|
+
assert self._regex is not None # kind == "regex"
|
|
68
|
+
return self._regex.match(path) is not None
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
methods = "*" if self.methods is None else ",".join(sorted(self.methods))
|
|
72
|
+
return f"PublicRoute({self.pattern!r}, kind={self.kind!r}, methods={methods})"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PublicRouteRegistry:
|
|
76
|
+
"""Aggregates every module's :class:`PublicRoute` rules.
|
|
77
|
+
|
|
78
|
+
Populated once during boot (``register_public_routes`` hook) and read on
|
|
79
|
+
every unauthenticated request by ``AuthMiddleware`` — effectively immutable
|
|
80
|
+
after the registration phase.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
self._routes: list[PublicRoute] = []
|
|
85
|
+
|
|
86
|
+
def add(
|
|
87
|
+
self,
|
|
88
|
+
route: PublicRoute | str,
|
|
89
|
+
*,
|
|
90
|
+
methods: Iterable[str] | None = None,
|
|
91
|
+
kind: _MatchKind = "prefix",
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Register a rule — either a prebuilt :class:`PublicRoute` or a pattern.
|
|
94
|
+
|
|
95
|
+
Passing a string builds a :class:`PublicRoute` from ``methods``/``kind``;
|
|
96
|
+
passing a :class:`PublicRoute` ignores those keyword arguments.
|
|
97
|
+
"""
|
|
98
|
+
if isinstance(route, PublicRoute):
|
|
99
|
+
self._routes.append(route)
|
|
100
|
+
else:
|
|
101
|
+
self._routes.append(PublicRoute(route, methods=methods, kind=kind))
|
|
102
|
+
|
|
103
|
+
def add_prefix(self, prefix: str, *, methods: Iterable[str] | None = None) -> None:
|
|
104
|
+
"""Exempt any path starting with *prefix*."""
|
|
105
|
+
self._routes.append(PublicRoute(prefix, methods=methods, kind="prefix"))
|
|
106
|
+
|
|
107
|
+
def add_exact(self, path: str, *, methods: Iterable[str] | None = None) -> None:
|
|
108
|
+
"""Exempt exactly *path*."""
|
|
109
|
+
self._routes.append(PublicRoute(path, methods=methods, kind="exact"))
|
|
110
|
+
|
|
111
|
+
def add_suffix(self, suffix: str, *, methods: Iterable[str] | None = None) -> None:
|
|
112
|
+
"""Exempt any path ending with *suffix*."""
|
|
113
|
+
self._routes.append(PublicRoute(suffix, methods=methods, kind="suffix"))
|
|
114
|
+
|
|
115
|
+
def add_regex(self, pattern: str, *, methods: Iterable[str] | None = None) -> None:
|
|
116
|
+
"""Exempt any path whose start matches *pattern* (``re.match`` semantics)."""
|
|
117
|
+
self._routes.append(PublicRoute(pattern, methods=methods, kind="regex"))
|
|
118
|
+
|
|
119
|
+
def matches(self, method: str, path: str) -> bool:
|
|
120
|
+
"""Return ``True`` if any registered rule exempts *method* + *path*."""
|
|
121
|
+
return any(route.matches(method, path) for route in self._routes)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def routes(self) -> list[PublicRoute]:
|
|
125
|
+
"""All registered rules (a copy — mutating it doesn't affect the registry)."""
|
|
126
|
+
return list(self._routes)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = ["PublicRoute", "PublicRouteRegistry"]
|
|
@@ -27,6 +27,7 @@ if TYPE_CHECKING:
|
|
|
27
27
|
from simple_module_core.menu import MenuRegistry
|
|
28
28
|
from simple_module_core.module import ModuleBase
|
|
29
29
|
from simple_module_core.permissions import PermissionRegistry
|
|
30
|
+
from simple_module_core.public_routes import PublicRouteRegistry
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@dataclass(frozen=True, slots=True)
|
|
@@ -40,6 +41,7 @@ class Services:
|
|
|
40
41
|
permissions: PermissionRegistry
|
|
41
42
|
feature_flags: FeatureFlagRegistry
|
|
42
43
|
health_registry: HealthRegistry
|
|
44
|
+
public_routes: PublicRouteRegistry
|
|
43
45
|
i18n_registry: I18nRegistry
|
|
44
46
|
inertia_config: InertiaConfig
|
|
45
47
|
modules: tuple[ModuleBase, ...]
|
|
@@ -101,6 +101,30 @@ class TestModuleNewHooks:
|
|
|
101
101
|
mod = DummyModule()
|
|
102
102
|
mod.register_settings(None)
|
|
103
103
|
|
|
104
|
+
async def test_register_public_routes_default_noop(self):
|
|
105
|
+
from simple_module_core.public_routes import PublicRouteRegistry
|
|
106
|
+
|
|
107
|
+
mod = DummyModule()
|
|
108
|
+
reg = PublicRouteRegistry()
|
|
109
|
+
mod.register_public_routes(reg)
|
|
110
|
+
assert reg.routes == []
|
|
111
|
+
|
|
112
|
+
async def test_register_public_routes_override(self):
|
|
113
|
+
from simple_module_core.public_routes import PublicRouteRegistry
|
|
114
|
+
|
|
115
|
+
class ModWithPublic(ModuleBase):
|
|
116
|
+
meta = ModuleMeta(name="WithPublic")
|
|
117
|
+
|
|
118
|
+
def register_public_routes(self, registry):
|
|
119
|
+
registry.add_prefix("/api/with-public/stac")
|
|
120
|
+
registry.add_regex(r"/api/with-public/datasets/[^/]+/tilejson$", methods={"GET"})
|
|
121
|
+
|
|
122
|
+
reg = PublicRouteRegistry()
|
|
123
|
+
ModWithPublic().register_public_routes(reg)
|
|
124
|
+
assert reg.matches("GET", "/api/with-public/stac/collections")
|
|
125
|
+
assert reg.matches("GET", "/api/with-public/datasets/9/tilejson")
|
|
126
|
+
assert not reg.matches("PATCH", "/api/with-public/datasets/9/tilejson")
|
|
127
|
+
|
|
104
128
|
|
|
105
129
|
class TestModuleAssetHooks:
|
|
106
130
|
async def test_template_dirs_default_empty(self):
|
|
@@ -193,6 +193,36 @@ class TestSM018InertiaApiCalls:
|
|
|
193
193
|
assert results == []
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
class TestSM007EmptyModules:
|
|
197
|
+
"""SM007 fires only when a module overrides no registration hooks at all."""
|
|
198
|
+
|
|
199
|
+
def _diags(self, modules):
|
|
200
|
+
from simple_module_core.diagnostics._module import ModuleDiagnostics
|
|
201
|
+
|
|
202
|
+
return list(ModuleDiagnostics()._check_empty_modules(modules))
|
|
203
|
+
|
|
204
|
+
async def test_fires_when_no_hooks_overridden(self):
|
|
205
|
+
from simple_module_core.module import ModuleBase, ModuleMeta
|
|
206
|
+
|
|
207
|
+
class Bare(ModuleBase):
|
|
208
|
+
meta = ModuleMeta(name="Bare")
|
|
209
|
+
|
|
210
|
+
results = self._diags([Bare()])
|
|
211
|
+
assert len(results) == 1
|
|
212
|
+
assert results[0].code == "SM007"
|
|
213
|
+
|
|
214
|
+
async def test_silent_when_only_public_routes_registered(self):
|
|
215
|
+
from simple_module_core.module import ModuleBase, ModuleMeta
|
|
216
|
+
|
|
217
|
+
class PublicOnly(ModuleBase):
|
|
218
|
+
meta = ModuleMeta(name="PublicOnly")
|
|
219
|
+
|
|
220
|
+
def register_public_routes(self, registry):
|
|
221
|
+
registry.add_prefix("/api/public_only/stac")
|
|
222
|
+
|
|
223
|
+
assert self._diags([PublicOnly()]) == []
|
|
224
|
+
|
|
225
|
+
|
|
196
226
|
class TestSM019ViewsWithoutMenu:
|
|
197
227
|
"""SM019 fires when a module ships view routes but never registers a menu item."""
|
|
198
228
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Tests for PublicRouteRegistry — method-aware anonymous-access rules.
|
|
2
|
+
|
|
3
|
+
The registry is the extension point modules use (via
|
|
4
|
+
``ModuleBase.register_public_routes``) to declare routes the auth layer must
|
|
5
|
+
let through unauthenticated. Unlike the legacy provider ``get_public_paths``
|
|
6
|
+
contract, a rule can be scoped to specific HTTP methods so a read route nested
|
|
7
|
+
under a mutation-bearing prefix can be exempted without opening the mutations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from simple_module_core.public_routes import PublicRoute, PublicRouteRegistry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestPublicRouteMatching:
|
|
16
|
+
def test_prefix_matches_any_subpath(self):
|
|
17
|
+
route = PublicRoute("/api/gis/stac")
|
|
18
|
+
assert route.matches("GET", "/api/gis/stac")
|
|
19
|
+
assert route.matches("GET", "/api/gis/stac/collections")
|
|
20
|
+
assert not route.matches("GET", "/api/gis/datasets")
|
|
21
|
+
|
|
22
|
+
def test_exact_matches_only_full_path(self):
|
|
23
|
+
route = PublicRoute("/api/gis/catalog/search", kind="exact")
|
|
24
|
+
assert route.matches("GET", "/api/gis/catalog/search")
|
|
25
|
+
assert not route.matches("GET", "/api/gis/catalog/search/extra")
|
|
26
|
+
|
|
27
|
+
def test_suffix_matches_path_tail(self):
|
|
28
|
+
route = PublicRoute("/tilejson", kind="suffix")
|
|
29
|
+
assert route.matches("GET", "/api/gis/datasets/42/tilejson")
|
|
30
|
+
assert not route.matches("GET", "/api/gis/datasets/42/visibility")
|
|
31
|
+
|
|
32
|
+
def test_regex_is_anchored_at_start(self):
|
|
33
|
+
route = PublicRoute(r"/api/gis/datasets/[^/]+/tilejson$", kind="regex")
|
|
34
|
+
assert route.matches("GET", "/api/gis/datasets/42/tilejson")
|
|
35
|
+
assert not route.matches("GET", "/api/gis/datasets/42/tilejson/extra")
|
|
36
|
+
assert not route.matches("GET", "/prefix/api/gis/datasets/42/tilejson")
|
|
37
|
+
|
|
38
|
+
def test_methods_none_matches_every_verb(self):
|
|
39
|
+
route = PublicRoute("/api/gis/stac")
|
|
40
|
+
for method in ("GET", "POST", "PATCH", "DELETE"):
|
|
41
|
+
assert route.matches(method, "/api/gis/stac")
|
|
42
|
+
|
|
43
|
+
def test_methods_restrict_to_listed_verbs(self):
|
|
44
|
+
route = PublicRoute("/api/gis/datasets/", methods={"GET"})
|
|
45
|
+
assert route.matches("GET", "/api/gis/datasets/42/tilejson")
|
|
46
|
+
assert not route.matches("PATCH", "/api/gis/datasets/42/visibility")
|
|
47
|
+
assert not route.matches("POST", "/api/gis/datasets/42/reprocess")
|
|
48
|
+
|
|
49
|
+
def test_method_matching_is_case_insensitive(self):
|
|
50
|
+
route = PublicRoute("/api/gis/stac", methods={"get"})
|
|
51
|
+
assert route.matches("GET", "/api/gis/stac")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestPublicRouteRegistry:
|
|
55
|
+
def test_empty_registry_matches_nothing(self):
|
|
56
|
+
registry = PublicRouteRegistry()
|
|
57
|
+
assert not registry.matches("GET", "/api/gis/stac")
|
|
58
|
+
|
|
59
|
+
def test_add_prefix(self):
|
|
60
|
+
registry = PublicRouteRegistry()
|
|
61
|
+
registry.add_prefix("/api/gis/ogc/")
|
|
62
|
+
assert registry.matches("GET", "/api/gis/ogc/collections")
|
|
63
|
+
assert not registry.matches("GET", "/api/gis/datasets")
|
|
64
|
+
|
|
65
|
+
def test_add_exact(self):
|
|
66
|
+
registry = PublicRouteRegistry()
|
|
67
|
+
registry.add_exact("/api/gis/catalog/search")
|
|
68
|
+
assert registry.matches("POST", "/api/gis/catalog/search")
|
|
69
|
+
assert not registry.matches("POST", "/api/gis/catalog/search/x")
|
|
70
|
+
|
|
71
|
+
def test_add_regex_with_method(self):
|
|
72
|
+
registry = PublicRouteRegistry()
|
|
73
|
+
registry.add_regex(r"/api/gis/datasets/[^/]+/tilejson$", methods={"GET"})
|
|
74
|
+
assert registry.matches("GET", "/api/gis/datasets/7/tilejson")
|
|
75
|
+
assert not registry.matches("PATCH", "/api/gis/datasets/7/tilejson")
|
|
76
|
+
|
|
77
|
+
def test_matches_is_true_if_any_route_matches(self):
|
|
78
|
+
registry = PublicRouteRegistry()
|
|
79
|
+
registry.add_prefix("/api/gis/ogc/")
|
|
80
|
+
registry.add_exact("/api/gis/catalog/search")
|
|
81
|
+
assert registry.matches("GET", "/api/gis/ogc/tiles")
|
|
82
|
+
assert registry.matches("GET", "/api/gis/catalog/search")
|
|
83
|
+
|
|
84
|
+
def test_routes_exposes_registered_rules(self):
|
|
85
|
+
registry = PublicRouteRegistry()
|
|
86
|
+
registry.add_prefix("/a")
|
|
87
|
+
registry.add_exact("/b")
|
|
88
|
+
assert len(registry.routes) == 2
|
|
89
|
+
assert all(isinstance(r, PublicRoute) for r in registry.routes)
|
|
90
|
+
|
|
91
|
+
def test_add_accepts_a_prebuilt_route(self):
|
|
92
|
+
registry = PublicRouteRegistry()
|
|
93
|
+
registry.add(PublicRoute("/api/gis/stac"))
|
|
94
|
+
assert registry.matches("GET", "/api/gis/stac")
|
|
@@ -29,6 +29,7 @@ class TestServices:
|
|
|
29
29
|
assert s.permissions is _SENTINEL_PERMS
|
|
30
30
|
assert s.feature_flags is _SENTINEL_FLAGS
|
|
31
31
|
assert s.health_registry is _SENTINEL_HEALTH
|
|
32
|
+
assert s.public_routes is _SENTINEL_PUBLIC_ROUTES
|
|
32
33
|
assert s.i18n_registry is _SENTINEL_I18N
|
|
33
34
|
assert s.inertia_config is _SENTINEL_INERTIA
|
|
34
35
|
assert s.modules == ()
|
|
@@ -41,6 +42,7 @@ _SENTINEL_MENU = object()
|
|
|
41
42
|
_SENTINEL_PERMS = object()
|
|
42
43
|
_SENTINEL_FLAGS = object()
|
|
43
44
|
_SENTINEL_HEALTH = object()
|
|
45
|
+
_SENTINEL_PUBLIC_ROUTES = object()
|
|
44
46
|
_SENTINEL_I18N = object()
|
|
45
47
|
_SENTINEL_INERTIA = object()
|
|
46
48
|
|
|
@@ -55,6 +57,7 @@ def _make_services() -> Services:
|
|
|
55
57
|
permissions=_SENTINEL_PERMS, # type: ignore[arg-type]
|
|
56
58
|
feature_flags=_SENTINEL_FLAGS, # type: ignore[arg-type]
|
|
57
59
|
health_registry=_SENTINEL_HEALTH, # type: ignore[arg-type]
|
|
60
|
+
public_routes=_SENTINEL_PUBLIC_ROUTES, # type: ignore[arg-type]
|
|
58
61
|
i18n_registry=_SENTINEL_I18N, # type: ignore[arg-type]
|
|
59
62
|
inertia_config=_SENTINEL_INERTIA, # type: ignore[arg-type]
|
|
60
63
|
modules=(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/__init__.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_coupling.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_i18n.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_migration.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_pages.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.17 → simple_module_core-0.0.18}/simple_module_core/diagnostics/_runner.py
RENAMED
|
File without changes
|
{simple_module_core-0.0.17 → simple_module_core-0.0.18}/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
|
{simple_module_core-0.0.17 → simple_module_core-0.0.18}/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
|