simple-module-core 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,89 @@
1
+ """Menu system — modules contribute menu items, filtered by user roles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import StrEnum
7
+ from typing import Literal
8
+
9
+ MenuItemMethod = Literal["get", "post"]
10
+
11
+
12
+ class MenuSection(StrEnum):
13
+ """Where the menu item appears in the UI."""
14
+
15
+ SIDEBAR = "sidebar"
16
+ ADMIN_SIDEBAR = "adminSidebar"
17
+ NAVBAR = "navbar"
18
+ USER_DROPDOWN = "userDropdown"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class MenuItem:
23
+ """A single navigation entry."""
24
+
25
+ label: str
26
+ url: str
27
+ icon: str = ""
28
+ order: int = 0
29
+ section: MenuSection = MenuSection.SIDEBAR
30
+ requires_auth: bool = True
31
+ roles: list[str] = field(default_factory=list)
32
+ """Empty list = visible to all authenticated users."""
33
+ method: MenuItemMethod = "get"
34
+ """HTTP method used when the item is activated. ``"post"`` renders as an
35
+ Inertia form submission so the target endpoint can be POST-only (e.g. logout)."""
36
+
37
+
38
+ class MenuRegistry:
39
+ """Collects menu items from all modules and filters them per-request."""
40
+
41
+ def __init__(self) -> None:
42
+ self._items: list[MenuItem] = []
43
+ self._sorted: list[MenuItem] | None = None
44
+
45
+ def _invalidate(self) -> None:
46
+ self._sorted = None
47
+
48
+ def add(self, item: MenuItem) -> None:
49
+ self._items.append(item)
50
+ self._invalidate()
51
+
52
+ def add_many(self, items: list[MenuItem]) -> None:
53
+ self._items.extend(items)
54
+ self._invalidate()
55
+
56
+ @property
57
+ def all_items(self) -> list[MenuItem]:
58
+ if self._sorted is None:
59
+ self._sorted = sorted(self._items, key=lambda i: i.order)
60
+ return self._sorted
61
+
62
+ def get_for_user(
63
+ self,
64
+ *,
65
+ is_authenticated: bool,
66
+ roles: list[str] | None = None,
67
+ ) -> dict[str, list[dict]]:
68
+ """Return menu items grouped by section, filtered by auth/roles.
69
+
70
+ Returns a dict ready to be serialized into Inertia shared props.
71
+ """
72
+ roles = roles or []
73
+ result: dict[str, list[dict]] = {s.value: [] for s in MenuSection}
74
+
75
+ for item in self.all_items:
76
+ if item.requires_auth and not is_authenticated:
77
+ continue
78
+ if item.roles and not any(r in item.roles for r in roles):
79
+ continue
80
+ result[item.section.value].append(
81
+ {
82
+ "label": item.label,
83
+ "url": item.url,
84
+ "icon": item.icon,
85
+ "method": item.method,
86
+ }
87
+ )
88
+
89
+ return result
@@ -0,0 +1,179 @@
1
+ """Module base class and metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from fastapi import APIRouter, FastAPI
12
+
13
+ from simple_module_core.events import EventBus
14
+ from simple_module_core.feature_flags import FeatureFlagRegistry
15
+ from simple_module_core.health import HealthRegistry
16
+ from simple_module_core.menu import MenuRegistry
17
+ from simple_module_core.permissions import PermissionRegistry
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ModuleMeta:
22
+ """Metadata describing a module."""
23
+
24
+ name: str
25
+ route_prefix: str = ""
26
+ view_prefix: str = ""
27
+ depends_on: list[str] = field(default_factory=list)
28
+ version: str = "1.0.0"
29
+ requires_framework: str | None = None
30
+ """PEP 440 specifier for the framework API version this module supports.
31
+
32
+ Example: ``">=1.0,<2.0"``. When set, the module is rejected at boot if the
33
+ installed ``simple_module_core.FRAMEWORK_API_VERSION`` does not satisfy it.
34
+ When ``None``, no compatibility check is performed (legacy modules).
35
+ """
36
+
37
+
38
+ class ModuleBase(ABC):
39
+ """Base class for all modules.
40
+
41
+ Subclasses override only the methods they need.
42
+ Every method has a default no-op implementation.
43
+
44
+ Hook execution order during app boot:
45
+
46
+ Phase 1: Bootstrap
47
+ - Load framework Settings
48
+ - Discover & topological-sort modules
49
+ - Run diagnostics (dev only)
50
+
51
+ Phase 2: App creation
52
+ - Create FastAPI app
53
+ - Store framework registries + settings on app.state
54
+
55
+ Phase 3: Module settings
56
+ - register_settings(app)
57
+
58
+ Phase 4: Module registrations
59
+ - register_menu_items(registry)
60
+ - register_permissions(registry)
61
+ - register_feature_flags(registry)
62
+ - register_event_handlers(bus)
63
+ - register_health_checks(registry)
64
+
65
+ Phase 5: Database
66
+ - init_db() -> DatabaseState
67
+ - register_listeners(db_state)
68
+
69
+ Phase 6: App wiring
70
+ - Inertia setup
71
+ - register_exception_handlers(app)
72
+ - Middleware pipeline (register_middleware per module)
73
+ - register_routes(api_router, view_router)
74
+ - Mount health router + static files
75
+
76
+ Phase 7: Runtime (async)
77
+ - on_startup(app) — per module, in dependency order
78
+ - on_shutdown(app) — per module, reverse order
79
+ """
80
+
81
+ meta: ModuleMeta
82
+
83
+ # ── Settings ─────────────────────────────────────────────
84
+
85
+ def register_settings(self, app: FastAPI) -> None:
86
+ """Load module-specific settings and store on ``app.state``.
87
+
88
+ Called before all other registration hooks. Convention:
89
+ store as ``app.state.<module_name_lower>_settings``.
90
+ """
91
+
92
+ # ── Service Registration ──────────────────────────────────
93
+
94
+ def register_routes(
95
+ self,
96
+ api_router: APIRouter,
97
+ view_router: APIRouter,
98
+ ) -> None:
99
+ """Register API endpoints and Inertia view routes."""
100
+
101
+ def register_menu_items(self, registry: MenuRegistry) -> None:
102
+ """Contribute menu items visible in the UI."""
103
+
104
+ def register_permissions(self, registry: PermissionRegistry) -> None:
105
+ """Declare permissions this module uses."""
106
+
107
+ def register_feature_flags(self, registry: FeatureFlagRegistry) -> None:
108
+ """Declare feature flags this module exposes."""
109
+
110
+ def register_event_handlers(self, bus: EventBus) -> None:
111
+ """Subscribe to events published by other modules."""
112
+
113
+ def register_health_checks(self, registry: HealthRegistry) -> None:
114
+ """Contribute health checks for the ``/health/ready`` endpoint."""
115
+
116
+ def register_middleware(self, app: FastAPI) -> None:
117
+ """Add middleware to the application.
118
+
119
+ Called after core middleware (session, security headers) positioning
120
+ is established but before the app starts. Modules that need to
121
+ inject middleware (e.g. auth) override this method.
122
+ """
123
+
124
+ def register_exception_handlers(self, app: FastAPI) -> None:
125
+ """Register custom exception handlers on the application.
126
+
127
+ Called after framework exception handlers are set up.
128
+ """
129
+
130
+ # ── Asset contribution (templates & static files) ─────────
131
+
132
+ def template_dirs(self) -> list[Path]:
133
+ """Return Jinja2 template directories this module contributes.
134
+
135
+ The host aggregates all modules' template dirs (plus its own) into the
136
+ Jinja2 loader, so templates in ``<module>/templates/`` become resolvable
137
+ from any module. Return an empty list (default) to contribute none.
138
+
139
+ Each path is typically computed via
140
+ ``importlib.resources.files(__package__) / "templates"`` so it resolves
141
+ correctly when the module is installed as a PyPI package.
142
+ """
143
+ return []
144
+
145
+ def static_mounts(self) -> dict[str, Path]:
146
+ """Return static file mounts this module contributes.
147
+
148
+ Mapping of URL prefix → filesystem directory. The host mounts each entry
149
+ via ``StaticFiles`` during app boot. Convention: prefix with
150
+ ``/modules/<name>/static`` to avoid collisions with the host's own
151
+ ``/static`` mount. Return an empty dict (default) to contribute none.
152
+ """
153
+ return {}
154
+
155
+ def locale_dirs(self) -> dict[str, Path]:
156
+ """Return ``{namespace: directory}`` mapping for locale JSON files.
157
+
158
+ Default returns an empty dict. Override to contribute a module's
159
+ locales::
160
+
161
+ return {
162
+ "products": importlib.resources.files(__package__) / "locales"
163
+ }
164
+
165
+ The namespace becomes the key prefix in the merged i18n registry.
166
+ A file ``locales/en.json`` containing ``{"browse": {"title": "X"}}``
167
+ becomes the key ``products.browse.title`` at runtime.
168
+
169
+ Convention: use the module's lowercase name as the namespace.
170
+ """
171
+ return {}
172
+
173
+ # ── Lifecycle ─────────────────────────────────────────────
174
+
175
+ async def on_startup(self, app: FastAPI) -> None:
176
+ """Called after all modules are registered, during app startup."""
177
+
178
+ async def on_shutdown(self, app: FastAPI) -> None:
179
+ """Called during app shutdown."""
@@ -0,0 +1,121 @@
1
+ """Permission registry — modules declare the permissions they use."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ WILDCARD = "*"
8
+
9
+ # Default role→permission mapping. Admin gets all permissions via the wildcard.
10
+ # Additional mappings are added at registration time via PermissionRegistry.map_role.
11
+ DEFAULT_ROLE_PERMISSIONS: dict[str, list[str]] = {
12
+ "admin": [WILDCARD],
13
+ }
14
+
15
+
16
+ @dataclass
17
+ class PermissionGroup:
18
+ """A named group of related permissions (typically one per module)."""
19
+
20
+ name: str
21
+ permissions: list[str] = field(default_factory=list)
22
+
23
+
24
+ class PermissionRegistry:
25
+ """Central registry of all permissions across all modules.
26
+
27
+ Effectively immutable after module-registration (boot phase). The computed
28
+ views ``all_permissions`` and ``role_map`` are read on every authenticated
29
+ request by ``InertiaLayoutDataMiddleware`` — cache them and invalidate on
30
+ every mutation.
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ self._groups: dict[str, PermissionGroup] = {}
35
+ self._role_map: dict[str, set[str]] = {}
36
+ self._all_permissions_cache: list[str] | None = None
37
+ self._role_map_cache: dict[str, list[str]] | None = None
38
+
39
+ def _invalidate(self) -> None:
40
+ self._all_permissions_cache = None
41
+ self._role_map_cache = None
42
+
43
+ def add_group(self, name: str, permissions: list[str]) -> None:
44
+ """Register a group of related permissions."""
45
+ if name in self._groups:
46
+ self._groups[name].permissions.extend(permissions)
47
+ else:
48
+ self._groups[name] = PermissionGroup(name=name, permissions=list(permissions))
49
+ self._invalidate()
50
+
51
+ def add(self, permission: str) -> None:
52
+ """Register a single permission (auto-grouped by prefix before '.')."""
53
+ group_name = permission.split(".")[0] if "." in permission else "general"
54
+ if group_name not in self._groups:
55
+ self._groups[group_name] = PermissionGroup(name=group_name)
56
+ if permission not in self._groups[group_name].permissions:
57
+ self._groups[group_name].permissions.append(permission)
58
+ self._invalidate()
59
+
60
+ @property
61
+ def all_permissions(self) -> list[str]:
62
+ """All registered permission strings, sorted."""
63
+ if self._all_permissions_cache is None:
64
+ perms: set[str] = set()
65
+ for group in self._groups.values():
66
+ perms.update(group.permissions)
67
+ self._all_permissions_cache = sorted(perms)
68
+ return self._all_permissions_cache
69
+
70
+ @property
71
+ def groups(self) -> list[PermissionGroup]:
72
+ return list(self._groups.values())
73
+
74
+ def has(self, permission: str) -> bool:
75
+ return any(permission in g.permissions for g in self._groups.values())
76
+
77
+ def map_role(self, role: str, permissions: list[str]) -> None:
78
+ """Register a role→permission mapping.
79
+
80
+ Merges *permissions* into the existing set for *role* so that multiple
81
+ calls from different modules accumulate rather than overwrite.
82
+ """
83
+ if role not in self._role_map:
84
+ self._role_map[role] = set()
85
+ self._role_map[role].update(permissions)
86
+ self._invalidate()
87
+
88
+ @property
89
+ def role_map(self) -> dict[str, list[str]]:
90
+ """Merged role→permission mapping (``DEFAULT_ROLE_PERMISSIONS`` + module maps)."""
91
+ if self._role_map_cache is None:
92
+ merged: dict[str, list[str]] = {
93
+ role: list(perms) for role, perms in DEFAULT_ROLE_PERMISSIONS.items()
94
+ }
95
+ for role, perms in self._role_map.items():
96
+ if role in merged:
97
+ merged[role] = list(set(merged[role]) | perms)
98
+ else:
99
+ merged[role] = list(perms)
100
+ self._role_map_cache = merged
101
+ return self._role_map_cache
102
+
103
+ def get_permissions_for_roles(
104
+ self,
105
+ roles: list[str],
106
+ role_permission_map: dict[str, list[str]] | None = None,
107
+ ) -> set[str]:
108
+ """Resolve permissions for a set of roles.
109
+
110
+ If role_permission_map is None, 'admin' gets all permissions,
111
+ other roles get none. Override for richer mapping.
112
+ """
113
+ if role_permission_map is None:
114
+ if "admin" in roles:
115
+ return set(self.all_permissions)
116
+ return set()
117
+
118
+ result: set[str] = set()
119
+ for role in roles:
120
+ result.update(role_permission_map.get(role, []))
121
+ return result
File without changes
@@ -0,0 +1,45 @@
1
+ """Framework-scoped singleton container.
2
+
3
+ Stored as ``app.state.sm`` during :func:`create_app`. Consumers read
4
+ ``request.app.state.sm.<field>`` instead of reaching for loose
5
+ ``app.state`` attributes — gives us typing, discoverability, and a
6
+ single place to see what the framework owns.
7
+
8
+ Frozen + slotted by design: Services is built once at boot and never
9
+ mutated. Slots reject attribute additions, which is how the previous
10
+ ``app.state`` shape grew unbounded.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from inertia import InertiaConfig
20
+ from simple_module_db.session import DatabaseState
21
+ from simple_module_hosting.settings import Settings
22
+
23
+ from simple_module_core.events import EventBus
24
+ from simple_module_core.feature_flags import FeatureFlagRegistry
25
+ from simple_module_core.health import HealthRegistry
26
+ from simple_module_core.i18n import I18nRegistry
27
+ from simple_module_core.menu import MenuRegistry
28
+ from simple_module_core.module import ModuleBase
29
+ from simple_module_core.permissions import PermissionRegistry
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class Services:
34
+ """Framework singletons. One slot per owner, read-only after boot."""
35
+
36
+ settings: Settings
37
+ db: DatabaseState
38
+ event_bus: EventBus
39
+ menu_registry: MenuRegistry
40
+ permissions: PermissionRegistry
41
+ feature_flags: FeatureFlagRegistry
42
+ health_registry: HealthRegistry
43
+ i18n_registry: I18nRegistry
44
+ inertia_config: InertiaConfig
45
+ modules: tuple[ModuleBase, ...]
@@ -0,0 +1,67 @@
1
+ """Framework API version compatibility checks for modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Sequence
7
+
8
+ from packaging.specifiers import InvalidSpecifier, SpecifierSet
9
+ from packaging.version import Version
10
+
11
+ from simple_module_core.exceptions import FrameworkVersionError
12
+ from simple_module_core.module import ModuleBase
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ FRAMEWORK_API_VERSION = "1.0.0"
17
+ """Current public API version of simple_module_core.
18
+
19
+ Modules declare a compatible range via ``ModuleMeta.requires_framework``
20
+ (PEP 440 specifier, e.g. ``">=1.0,<2.0"``). Bumping this constant's major
21
+ component signals a breaking change in the module contract — ``ModuleBase``,
22
+ registries, the event bus, ``create_module_base``, or the model mixins.
23
+ """
24
+
25
+ # Pre-parsed for the default compatibility check so boot doesn't re-parse
26
+ # this constant on every call. The custom-version path below still parses
27
+ # caller-supplied strings lazily.
28
+ _FRAMEWORK_VERSION = Version(FRAMEWORK_API_VERSION)
29
+
30
+
31
+ def check_framework_compatibility(
32
+ modules: Sequence[ModuleBase],
33
+ framework_version: str = FRAMEWORK_API_VERSION,
34
+ ) -> None:
35
+ """Fail fast if any module's ``requires_framework`` spec rejects the framework version.
36
+
37
+ Modules with ``requires_framework=None`` are skipped (unversioned modules pass).
38
+ Malformed specifiers are reported as failures rather than letting a cryptic
39
+ ``InvalidSpecifier`` bubble up from deep inside ``packaging``.
40
+ """
41
+ current = (
42
+ _FRAMEWORK_VERSION
43
+ if framework_version == FRAMEWORK_API_VERSION
44
+ else Version(framework_version)
45
+ )
46
+ failures: list[tuple[str, str, str]] = []
47
+
48
+ for mod in modules:
49
+ spec_str = mod.meta.requires_framework
50
+ if spec_str is None:
51
+ continue
52
+ try:
53
+ spec = SpecifierSet(spec_str)
54
+ except InvalidSpecifier as exc:
55
+ failures.append((mod.meta.name, spec_str, f"invalid version specifier ({exc})"))
56
+ continue
57
+ if current not in spec:
58
+ failures.append(
59
+ (
60
+ mod.meta.name,
61
+ spec_str,
62
+ f"framework {framework_version} does not satisfy '{spec_str}'",
63
+ )
64
+ )
65
+
66
+ if failures:
67
+ raise FrameworkVersionError(framework_version, failures)
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple_module_core
3
+ Version: 0.0.1
4
+ Summary: Module-system primitives for the simple_module framework — ModuleBase, discovery, diagnostics, events
5
+ Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
+ Project-URL: Repository, https://github.com/antosubash/simple_module_python
7
+ Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
8
+ Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
9
+ Author-email: Anto Subash <antosubash@live.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: fastapi,modular-monolith,module-discovery,plugin-system,simple-module
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: babel>=2.14
25
+ Requires-Dist: fastapi>=0.115
26
+ Requires-Dist: packaging>=23.0
27
+ Requires-Dist: pydantic-settings>=2.0
28
+ Requires-Dist: pydantic>=2.0
29
+ Requires-Dist: pyee>=12.0
30
+ Description-Content-Type: text/markdown
31
+
32
+ # simple_module_core
33
+
34
+ Module-system primitives for the [simple_module](https://github.com/antosubash/simple_module_python) framework — a modular-monolith for Python/FastAPI where each feature is a plugin package discovered at boot.
35
+
36
+ This package defines `ModuleBase`, the `ModuleMeta` descriptor, the `discover_modules()` entry-point loader, topological dependency sorting, event bus primitives, and the diagnostic codes (`SM001`–`SM017`) used by `make doctor`.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install simple_module_core
42
+ ```
43
+
44
+ You usually don't install this directly — it's pulled in by `simple_module_hosting` and every `simple_module_*` module.
45
+
46
+ ## What it provides
47
+
48
+ - `ModuleBase` — the subclass every module extends to opt into lifecycle hooks.
49
+ - `ModuleMeta` — required `meta = ModuleMeta(name=..., depends_on=...)` attribute on each module.
50
+ - `discover_modules()` — loads all `[project.entry-points.simple_module]` modules, topologically sorts by `depends_on`.
51
+ - Diagnostic registry — `SM001` missing meta, `SM003` orphan page, `SM008` duplicate name, `SM009` framework→plugin coupling violation, and ~ten others.
52
+ - Tiny event-bus (`pyee`) for decoupled module-to-module communication.
53
+
54
+ ## Usage
55
+
56
+ ```python
57
+ # modules/orders/orders/module.py
58
+ from simple_module_core import ModuleBase, ModuleMeta
59
+
60
+
61
+ class OrdersModule(ModuleBase):
62
+ meta = ModuleMeta(name="orders", depends_on=["users"])
63
+
64
+ def register_routes(self, api_router, view_router):
65
+ from .endpoints import api, views
66
+ api_router.include_router(api.router)
67
+ view_router.include_router(views.router)
68
+ ```
69
+
70
+ And in `pyproject.toml`:
71
+
72
+ ```toml
73
+ [project.entry-points.simple_module]
74
+ orders = "orders.module:OrdersModule"
75
+ ```
76
+
77
+ The host's `discover_modules()` call picks this up automatically at boot.
78
+
79
+ ## Depends on
80
+
81
+ - `fastapi`, `pydantic`, `pydantic-settings`, `pyee`, `babel`, `packaging`
82
+
83
+ ## License
84
+
85
+ MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
@@ -0,0 +1,29 @@
1
+ simple_module_core/__init__.py,sha256=e1Sf0Hy2YgOt_bl4Fs8-6qwSXWwAfbartRXQr5Z1vss,2087
2
+ simple_module_core/__main__.py,sha256=98rf_b9GsbcYoHvU9q3aondBuHvxVO-VMJPuohb0XNc,3096
3
+ simple_module_core/discovery.py,sha256=oeib4kJJXwqe0y5HczyMIRbubPHXerHv4RYRTYsuR3s,6776
4
+ simple_module_core/dotenv.py,sha256=ubQ-hS-yMnBXkW1Sh0yTgLzlcHPOZ1P78VJXCJ7N2fM,1461
5
+ simple_module_core/environments.py,sha256=OY7Hy9FcGjit3RRnROxUJHynMi5Wq9G1_zrjwYb0Jfw,663
6
+ simple_module_core/events.py,sha256=3klVq41t08hHs6rp59FolnH0gJQjiyvTiuWAaBEgm7k,3015
7
+ simple_module_core/exceptions.py,sha256=X8MEs-NodkS5tTcaO24BjOeAkmWXdODRGukE6ys1q64,2034
8
+ simple_module_core/feature_flags.py,sha256=I2nkIX6dEvG8in9N4JR3VrDnCTG8y1pWzM3KJJK3FiU,6666
9
+ simple_module_core/health.py,sha256=UPeWmjh607JnKG1XXnOiGKa4HHxd5x51EZR8HfuyRyU,982
10
+ simple_module_core/i18n.py,sha256=shpcg7k8IVNofiwxo30jhZ-tBzRG8XWiz2PP20CPFdw,10247
11
+ simple_module_core/menu.py,sha256=toaNvXzO-gL0bvpckSOGRWRavVy70BPH0lOQd_ZCfFE,2586
12
+ simple_module_core/module.py,sha256=CiaaAjFbf3dp-BJEti9W_Y3cPULG5paNAUk45TCxd-E,6588
13
+ simple_module_core/permissions.py,sha256=Z_QRC6WL1IyKjbeWWxj-RbyGJLwZU2H2NwBNrpwdHTg,4517
14
+ simple_module_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ simple_module_core/services.py,sha256=_BmZCT18ZaXMT1qif0CCt7Fi6mB7i2MqbniYleC5RBs,1585
16
+ simple_module_core/versioning.py,sha256=ii7gF5gcO1FV7qw0J86ksRS49kh3GMhUcoyX0Nrm0_g,2361
17
+ simple_module_core/diagnostics/__init__.py,sha256=t2RX37e9rIlBST-KJ2jysuniAZ0Eq-SuV2PvUnIdbio,874
18
+ simple_module_core/diagnostics/_coupling.py,sha256=xrS5yteuLIobxwBahxCRkCw3WopIG8WYdGZfkBAsB3Q,3023
19
+ simple_module_core/diagnostics/_i18n.py,sha256=DVthcWRee-zlR1BLemV3HYxFYZXTkVL9Kj2dqoE3iUQ,4852
20
+ simple_module_core/diagnostics/_inertia_api.py,sha256=OuvxaR7CKen-a5RVaKoPNobxrbIHuyU2SQ7ZBlLfUCw,2839
21
+ simple_module_core/diagnostics/_js_workspace.py,sha256=004aYAFHbP31j3Iy8x-0l_1Sj1f4CeIR35LO1sfxEIM,1250
22
+ simple_module_core/diagnostics/_migration.py,sha256=7D1kE88kDy7bZgu4y9oAJexVg3zb6i6fsZQJEJJsZpk,1528
23
+ simple_module_core/diagnostics/_module.py,sha256=fQTBZ1y0NajB6b8mhOEu-IknTjnRaVdYtYCo-CeTOr0,10165
24
+ simple_module_core/diagnostics/_runner.py,sha256=E9NraAj_3i5CCoRA-5PcpWxf-Tk0GdXtx_jEcbWrfmI,2892
25
+ simple_module_core/diagnostics/_types.py,sha256=EQucVU24ktuxqupot3Gf10HolFmdmM-QDwPL6tolJJc,886
26
+ simple_module_core-0.0.1.dist-info/METADATA,sha256=1G2stmMayQBX87RItlTYgGOFy5AbsD7uWC-t2LUPI1k,3377
27
+ simple_module_core-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
28
+ simple_module_core-0.0.1.dist-info/licenses/LICENSE,sha256=Yn66lhLklsF5p7pa85_ksQrJ79Q-FgOaUAHevLBjer4,1068
29
+ simple_module_core-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anto Subash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.