splent-framework 1.6.2__tar.gz → 1.7.0__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.
- {splent_framework-1.6.2/src/splent_framework.egg-info → splent_framework-1.7.0}/PKG-INFO +1 -1
- {splent_framework-1.6.2 → splent_framework-1.7.0}/pyproject.toml +1 -1
- splent_framework-1.7.0/src/splent_framework/admin/__init__.py +29 -0
- splent_framework-1.7.0/src/splent_framework/admin/registry.py +149 -0
- splent_framework-1.7.0/src/splent_framework/assets/asset_registry.py +68 -0
- splent_framework-1.7.0/src/splent_framework/hooks/template_hooks.py +43 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/feature_loader.py +14 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/feature_manager.py +35 -12
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/jinja_manager.py +2 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/locale_manager.py +10 -4
- splent_framework-1.7.0/src/splent_framework/nav/nav_registry.py +50 -0
- splent_framework-1.7.0/src/splent_framework/settings/__init__.py +0 -0
- splent_framework-1.7.0/src/splent_framework/settings/settings_schema.py +103 -0
- splent_framework-1.7.0/src/splent_framework/signals/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/signals/signal_utils.py +16 -4
- splent_framework-1.7.0/src/splent_framework/utils/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0/src/splent_framework.egg-info}/PKG-INFO +1 -1
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework.egg-info/SOURCES.txt +8 -0
- splent_framework-1.6.2/src/splent_framework/hooks/template_hooks.py +0 -36
- {splent_framework-1.6.2 → splent_framework-1.7.0}/LICENSE +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/README.md +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/setup.cfg +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/_init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/app_factory.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/blueprints → splent_framework-1.7.0/src/splent_framework/assets}/__init__.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/bootstraps → splent_framework-1.7.0/src/splent_framework/blueprints}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/blueprints/base_blueprint.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/configuration → splent_framework-1.7.0/src/splent_framework/bootstraps}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/bootstraps/locustfile_bootstrap.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/context → splent_framework-1.7.0/src/splent_framework/configuration}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/configuration/configuration.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/configuration/default_config.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/decorators → splent_framework-1.7.0/src/splent_framework/context}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/context/context_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/db.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/environment → splent_framework-1.7.0/src/splent_framework/decorators}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/decorators/decorators.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/fixtures → splent_framework-1.7.0/src/splent_framework/environment}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/environment/host.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/helpers → splent_framework-1.7.0/src/splent_framework/fixtures}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/fixtures/fixtures.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/hooks → splent_framework-1.7.0/src/splent_framework/helpers}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/helpers/test_helpers_auth.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/locust → splent_framework-1.7.0/src/splent_framework/hooks}/__init__.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/managers → splent_framework-1.7.0/src/splent_framework/locust}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/locust/common.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/migrations → splent_framework-1.7.0/src/splent_framework/managers}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/config_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/db_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/error_handler_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/feature_order.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/logging_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/migration_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/namespace_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/session_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/task_queue_manager.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/namespaces → splent_framework-1.7.0/src/splent_framework/migrations}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/migrations/feature_env.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/repositories → splent_framework-1.7.0/src/splent_framework/namespaces}/__init__.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/resources → splent_framework-1.7.0/src/splent_framework/nav}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/model_extender.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/parser.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/registry.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/validator.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/repositories/BaseRepository.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/seeders → splent_framework-1.7.0/src/splent_framework/repositories}/__init__.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/selenium → splent_framework-1.7.0/src/splent_framework/resources}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/resources/generic_resource.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/seeders/BaseSeeder.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/serialisers → splent_framework-1.7.0/src/splent_framework/seeders}/__init__.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/services → splent_framework-1.7.0/src/splent_framework/selenium}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/selenium/common.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/signals → splent_framework-1.7.0/src/splent_framework/serialisers}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/serialisers/serializer.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/services/BaseService.py +0 -0
- {splent_framework-1.6.2/src/splent_framework/utils → splent_framework-1.7.0/src/splent_framework/services}/__init__.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/services/service_locator.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/signals/registry.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/utils/app_loader.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/utils/feature_utils.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/utils/form_helpers.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/utils/path_utils.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/utils/pyproject_reader.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework.egg-info/dependency_links.txt +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework.egg-info/requires.txt +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework.egg-info/top_level.txt +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_base_blueprint_security.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_base_repository.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_config_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_config_manager_return.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_context_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_default_config.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_error_handler_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_feature_manager_parse.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_feature_order.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_feature_utils.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_generic_resource.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_migration_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_namespace_manager.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_pyproject_reader.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_pyproject_reader_env.py +0 -0
- {splent_framework-1.6.2 → splent_framework-1.7.0}/tests/test_refinement.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splent_framework
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: SPLENT-FRAMEWORK is a set of libraries for agile product development within SPLENT.
|
|
5
5
|
Author-email: DiversoLab <diversolab@us.es>
|
|
6
6
|
Project-URL: Homepage, https://github.com/diverso-lab/splent_framework
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "splent_framework"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.0"
|
|
8
8
|
description = "SPLENT-FRAMEWORK is a set of libraries for agile product development within SPLENT."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Admin resource registry — declare how models surface in the admin panel.
|
|
2
|
+
|
|
3
|
+
A content feature registers its model with :func:`register_admin_resource`
|
|
4
|
+
(typically from ``init_feature``); the ``admin`` feature reads the registry to
|
|
5
|
+
build a grouped, wp-admin-style menu and CRUD screens. Mirrors the singleton
|
|
6
|
+
pattern of ``splent_framework.hooks.template_hooks``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from splent_framework.admin.registry import (
|
|
10
|
+
WIDGETS,
|
|
11
|
+
AdminResource,
|
|
12
|
+
clear_admin_resources,
|
|
13
|
+
get_admin_groups,
|
|
14
|
+
get_admin_resource,
|
|
15
|
+
get_admin_resource_for_model,
|
|
16
|
+
get_admin_resources,
|
|
17
|
+
register_admin_resource,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"WIDGETS",
|
|
22
|
+
"AdminResource",
|
|
23
|
+
"register_admin_resource",
|
|
24
|
+
"get_admin_resource",
|
|
25
|
+
"get_admin_resource_for_model",
|
|
26
|
+
"get_admin_resources",
|
|
27
|
+
"get_admin_groups",
|
|
28
|
+
"clear_admin_resources",
|
|
29
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# splent_framework/admin/registry.py
|
|
2
|
+
#
|
|
3
|
+
# Admin resource registry — the analogue of WordPress `register_post_type`.
|
|
4
|
+
#
|
|
5
|
+
# A content feature registers its model as an *admin resource*, declaring how it
|
|
6
|
+
# should appear in the admin panel (label, icon, menu group, list columns,
|
|
7
|
+
# per-field widgets). The `admin` feature reads this registry to build a curated,
|
|
8
|
+
# grouped, wp-admin-style menu and CRUD screens — instead of dumping every
|
|
9
|
+
# `db.Model` subclass in a flat list.
|
|
10
|
+
#
|
|
11
|
+
# NOTE: _resources is a module-level registry populated once at app startup
|
|
12
|
+
# (during feature registration / init_feature) and read-only during request
|
|
13
|
+
# handling — same contract as template_hooks.py. Not thread-safe for concurrent
|
|
14
|
+
# writes; do not call register_admin_resource() from request handlers.
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
|
|
18
|
+
# Widget hints the admin form renderer understands. Features may pass any of
|
|
19
|
+
# these per column via `field_widgets`; unknown columns fall back to an
|
|
20
|
+
# introspection-derived default chosen by the admin feature.
|
|
21
|
+
WIDGETS = (
|
|
22
|
+
"text", # single-line string
|
|
23
|
+
"textarea", # multi-line plain text
|
|
24
|
+
"richtext", # WYSIWYG / HTML body
|
|
25
|
+
"number",
|
|
26
|
+
"bool",
|
|
27
|
+
"date",
|
|
28
|
+
"datetime",
|
|
29
|
+
"select", # enum / fixed choices
|
|
30
|
+
"fk", # foreign key -> related record picker
|
|
31
|
+
"image", # media id / upload
|
|
32
|
+
"url",
|
|
33
|
+
"slug",
|
|
34
|
+
"color",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class AdminResource:
|
|
40
|
+
"""Metadata describing how a model is administered in the admin panel."""
|
|
41
|
+
|
|
42
|
+
model: type
|
|
43
|
+
name: str # url-safe key, e.g. "project"
|
|
44
|
+
label: str # singular human label, e.g. "Project"
|
|
45
|
+
label_plural: str # e.g. "Projects"
|
|
46
|
+
icon: str = "file" # feather icon name used in the menu
|
|
47
|
+
group: str = "Content" # menu section this resource is filed under
|
|
48
|
+
order: int = 100 # sort order within the group
|
|
49
|
+
list_columns: list[str] | None = None # None -> admin auto-picks
|
|
50
|
+
search_fields: list[str] = field(default_factory=list)
|
|
51
|
+
field_widgets: dict[str, str] = field(default_factory=dict) # column -> WIDGETS
|
|
52
|
+
readonly_fields: list[str] = field(default_factory=list)
|
|
53
|
+
scope: dict = field(default_factory=dict) # query filter — lets one shared
|
|
54
|
+
# table back several menu entries
|
|
55
|
+
# (e.g. {"collection": "event"})
|
|
56
|
+
create_defaults: dict = field(
|
|
57
|
+
default_factory=dict
|
|
58
|
+
) # column values forced on create
|
|
59
|
+
feature: str | None = None # owning feature short name (provenance)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def model_name(self) -> str:
|
|
63
|
+
"""The SQLAlchemy class name — the key the CRUD routes resolve by."""
|
|
64
|
+
return self.model.__name__
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Module-level singleton registry (keyed by resource `name`).
|
|
68
|
+
_resources: dict[str, AdminResource] = {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def register_admin_resource(
|
|
72
|
+
model: type,
|
|
73
|
+
*,
|
|
74
|
+
name: str | None = None,
|
|
75
|
+
label: str | None = None,
|
|
76
|
+
label_plural: str | None = None,
|
|
77
|
+
icon: str = "file",
|
|
78
|
+
group: str = "Content",
|
|
79
|
+
order: int = 100,
|
|
80
|
+
list_columns: list[str] | None = None,
|
|
81
|
+
search_fields: list[str] | None = None,
|
|
82
|
+
field_widgets: dict[str, str] | None = None,
|
|
83
|
+
readonly_fields: list[str] | None = None,
|
|
84
|
+
scope: dict | None = None,
|
|
85
|
+
create_defaults: dict | None = None,
|
|
86
|
+
feature: str | None = None,
|
|
87
|
+
) -> AdminResource:
|
|
88
|
+
"""Register a model as an administered resource (additive, idempotent by name).
|
|
89
|
+
|
|
90
|
+
Call from a feature's ``init_feature(app)``. The owning content feature thus
|
|
91
|
+
declares its admin surface once; the admin panel renders it. Re-registering
|
|
92
|
+
the same ``name`` overrides the previous entry (last registration wins), which
|
|
93
|
+
lets a refinement feature curate another feature's resource.
|
|
94
|
+
"""
|
|
95
|
+
key = name or model.__name__.lower()
|
|
96
|
+
resolved_label = label or model.__name__
|
|
97
|
+
res = AdminResource(
|
|
98
|
+
model=model,
|
|
99
|
+
name=key,
|
|
100
|
+
label=resolved_label,
|
|
101
|
+
label_plural=label_plural or f"{resolved_label}s",
|
|
102
|
+
icon=icon,
|
|
103
|
+
group=group,
|
|
104
|
+
order=order,
|
|
105
|
+
list_columns=list_columns,
|
|
106
|
+
search_fields=search_fields or [],
|
|
107
|
+
field_widgets=field_widgets or {},
|
|
108
|
+
readonly_fields=readonly_fields or [],
|
|
109
|
+
scope=scope or {},
|
|
110
|
+
create_defaults=create_defaults or {},
|
|
111
|
+
feature=feature,
|
|
112
|
+
)
|
|
113
|
+
_resources[key] = res
|
|
114
|
+
return res
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_admin_resource(name: str) -> AdminResource | None:
|
|
118
|
+
"""Return a single registered resource by its url-safe name, or None."""
|
|
119
|
+
return _resources.get(name)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_admin_resource_for_model(model_name: str) -> AdminResource | None:
|
|
123
|
+
"""Return the resource whose model class name matches ``model_name``."""
|
|
124
|
+
for res in _resources.values():
|
|
125
|
+
if res.model_name == model_name:
|
|
126
|
+
return res
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_admin_resources() -> dict[str, AdminResource]:
|
|
131
|
+
"""Return all registered resources keyed by name (insertion order)."""
|
|
132
|
+
return dict(_resources)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_admin_groups() -> dict[str, list[AdminResource]]:
|
|
136
|
+
"""Return resources bucketed by menu group, each sorted by (order, label).
|
|
137
|
+
|
|
138
|
+
This is what the admin sidebar iterates to render a grouped, wp-admin-style
|
|
139
|
+
menu (a section per group, content types listed underneath).
|
|
140
|
+
"""
|
|
141
|
+
groups: dict[str, list[AdminResource]] = {}
|
|
142
|
+
for res in sorted(_resources.values(), key=lambda r: (r.order, r.label)):
|
|
143
|
+
groups.setdefault(res.group, []).append(res)
|
|
144
|
+
return groups
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def clear_admin_resources() -> None:
|
|
148
|
+
"""Remove all registered resources. Intended for test teardown."""
|
|
149
|
+
_resources.clear()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# splent_framework/assets/asset_registry.py
|
|
2
|
+
#
|
|
3
|
+
# SPL asset registry. Features DECLARE their stylesheets/scripts here (from
|
|
4
|
+
# init_feature) instead of hand-writing <link>/<script> tags in hooks or
|
|
5
|
+
# inlining JS into templates. The public shell renders get_assets("css") in
|
|
6
|
+
# <head> and get_assets("js") before </body>, DEDUPLICATED and ORDERED — so the
|
|
7
|
+
# cascade is deterministic and asset injection is consistent across features.
|
|
8
|
+
# No ad-hoc tags, no inline scripts: a feature ships a file under its blueprint's
|
|
9
|
+
# assets/ folder (served by the blueprint asset route) and registers it here.
|
|
10
|
+
#
|
|
11
|
+
# Cleared per app at feature-load (like the nav registry) so each product only
|
|
12
|
+
# emits the assets of ITS installed features. Registration is idempotent.
|
|
13
|
+
|
|
14
|
+
_assets: list = []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_asset(kind: str, endpoint: str, order: int = 100, **params) -> None:
|
|
18
|
+
"""Declare a static asset served by a feature blueprint's asset route.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
kind: "css" or "js".
|
|
22
|
+
endpoint: the blueprint asset endpoint, e.g. "slider.assets".
|
|
23
|
+
order: load order (lower first) — governs the cascade deterministically.
|
|
24
|
+
Convention: theme base ~0, features ~100, skins ~200 (win last).
|
|
25
|
+
**params: url_for params for the asset route, e.g.
|
|
26
|
+
subfolder="css", filename="slider.css".
|
|
27
|
+
"""
|
|
28
|
+
key = (kind, endpoint, tuple(sorted(params.items())))
|
|
29
|
+
for a in _assets:
|
|
30
|
+
if a["key"] == key:
|
|
31
|
+
return
|
|
32
|
+
_assets.append(
|
|
33
|
+
{
|
|
34
|
+
"key": key,
|
|
35
|
+
"kind": kind,
|
|
36
|
+
"endpoint": endpoint,
|
|
37
|
+
"params": params,
|
|
38
|
+
"order": order,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_assets(kind: str) -> list:
|
|
44
|
+
"""Resolved asset URLs of a kind, ordered by (order, endpoint) and deduped.
|
|
45
|
+
|
|
46
|
+
url_for is resolved at call time, so invoke this from the template (request
|
|
47
|
+
context). Assets whose route can't be resolved are skipped, not raised.
|
|
48
|
+
"""
|
|
49
|
+
from flask import url_for
|
|
50
|
+
|
|
51
|
+
seen = set()
|
|
52
|
+
out = []
|
|
53
|
+
for a in sorted(_assets, key=lambda x: (x["order"], x["endpoint"])):
|
|
54
|
+
if a["kind"] != kind:
|
|
55
|
+
continue
|
|
56
|
+
try:
|
|
57
|
+
href = url_for(a["endpoint"], **a["params"])
|
|
58
|
+
except Exception:
|
|
59
|
+
continue
|
|
60
|
+
if href not in seen:
|
|
61
|
+
seen.add(href)
|
|
62
|
+
out.append(href)
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def clear_assets() -> None:
|
|
67
|
+
"""Drop all registered assets. Called per app before feature load."""
|
|
68
|
+
_assets.clear()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# splent_framework/template_hooks.py
|
|
2
|
+
#
|
|
3
|
+
# NOTE: _hooks is a module-level registry populated once at app startup
|
|
4
|
+
# (during feature registration) and read-only during request handling.
|
|
5
|
+
# It is NOT thread-safe for concurrent writes; do not call
|
|
6
|
+
# register_template_hook() from request handlers or background threads.
|
|
7
|
+
|
|
8
|
+
# Each slot holds (order, func) pairs; get_template_hooks returns the funcs
|
|
9
|
+
# sorted by order (lower first), so slots like "home.section" compose features
|
|
10
|
+
# in a deterministic order regardless of feature load order.
|
|
11
|
+
_hooks: dict[str, list] = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_template_hook(name: str, func, order: int = 100) -> None:
|
|
15
|
+
"""Append a callback to a named hook slot (additive).
|
|
16
|
+
|
|
17
|
+
order: lower renders first within the slot (default 100).
|
|
18
|
+
"""
|
|
19
|
+
_hooks.setdefault(name, []).append((order, func))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def replace_template_hook(name: str, func, order: int = 100) -> None:
|
|
23
|
+
"""Replace ALL existing callbacks for a named hook slot.
|
|
24
|
+
|
|
25
|
+
Used by refinement features to override a base feature's hook content.
|
|
26
|
+
"""
|
|
27
|
+
_hooks[name] = [(order, func)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def remove_template_hook(name: str, func) -> None:
|
|
31
|
+
"""Remove a specific callback from a hook slot."""
|
|
32
|
+
if name in _hooks:
|
|
33
|
+
_hooks[name] = [(o, f) for (o, f) in _hooks[name] if f is not func]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_template_hooks(name: str) -> list:
|
|
37
|
+
"""Callbacks for a slot, ordered by their registration `order`."""
|
|
38
|
+
return [f for _, f in sorted(_hooks.get(name, []), key=lambda pair: pair[0])]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clear_hooks() -> None:
|
|
42
|
+
"""Remove all registered hooks. Intended for use in test teardown."""
|
|
43
|
+
_hooks.clear()
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/feature_loader.py
RENAMED
|
@@ -248,6 +248,7 @@ class FeatureIntegrator:
|
|
|
248
248
|
self._call_init(module, import_name)
|
|
249
249
|
self._apply_service_overrides(import_name)
|
|
250
250
|
self._register_blueprints(module, import_name)
|
|
251
|
+
self._register_context_vars(module, import_name)
|
|
251
252
|
self._register_translations(module, import_name)
|
|
252
253
|
self._register_commands(import_name)
|
|
253
254
|
|
|
@@ -358,6 +359,19 @@ class FeatureIntegrator:
|
|
|
358
359
|
elif self._strict:
|
|
359
360
|
raise FeatureError(f"{import_name} lacks init_feature(app)")
|
|
360
361
|
|
|
362
|
+
def _register_context_vars(self, module, import_name: str) -> None:
|
|
363
|
+
"""Collect the feature's ``inject_context_vars`` so the JinjaManager
|
|
364
|
+
context processor merges its returned dict into every template's context
|
|
365
|
+
on each request (see docs: framework/template-system, framework/
|
|
366
|
+
feature-loading). Without this the function is never called.
|
|
367
|
+
"""
|
|
368
|
+
fn = getattr(module, "inject_context_vars", None)
|
|
369
|
+
if fn is None:
|
|
370
|
+
return
|
|
371
|
+
if not hasattr(self._app, "context_processors"):
|
|
372
|
+
self._app.context_processors = []
|
|
373
|
+
self._app.context_processors.append(fn)
|
|
374
|
+
|
|
361
375
|
def _register_blueprints(self, module, import_name: str) -> None:
|
|
362
376
|
candidates = self._collect_candidate_modules(module, import_name)
|
|
363
377
|
registered = sum(
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/feature_manager.py
RENAMED
|
@@ -62,6 +62,18 @@ class FeatureManager:
|
|
|
62
62
|
return
|
|
63
63
|
self._registered = True
|
|
64
64
|
|
|
65
|
+
# The nav and asset registries are process-global; clear them before
|
|
66
|
+
# loading so the composed menu / injected assets reflect ONLY this
|
|
67
|
+
# product's installed features (no leakage when several products'
|
|
68
|
+
# create_app() run in one interpreter).
|
|
69
|
+
from splent_framework.assets.asset_registry import clear_assets
|
|
70
|
+
from splent_framework.nav.nav_registry import clear_nav_items
|
|
71
|
+
from splent_framework.settings.settings_schema import clear_settings_schemas
|
|
72
|
+
|
|
73
|
+
clear_nav_items()
|
|
74
|
+
clear_assets()
|
|
75
|
+
clear_settings_schemas()
|
|
76
|
+
|
|
65
77
|
splent_app = self._require_splent_app()
|
|
66
78
|
product_dir = os.path.join(PathUtils.get_working_dir(), splent_app)
|
|
67
79
|
|
|
@@ -121,27 +133,33 @@ class FeatureManager:
|
|
|
121
133
|
# ------------------------------------------------------------------
|
|
122
134
|
|
|
123
135
|
def _apply_template_overrides(self, registry) -> None:
|
|
124
|
-
"""
|
|
136
|
+
"""Make refiner templates win over the base feature's same-path templates.
|
|
125
137
|
|
|
126
|
-
Flask's DispatchingJinjaLoader searches blueprints in
|
|
127
|
-
|
|
128
|
-
We
|
|
138
|
+
Flask's DispatchingJinjaLoader searches blueprints in REGISTRATION order
|
|
139
|
+
(first registered wins), and a refiner loads AFTER its base — so the base
|
|
140
|
+
template would win. We prepend each refiner's template folder to the
|
|
141
|
+
app's Jinja loader so its override is resolved first.
|
|
129
142
|
"""
|
|
143
|
+
from jinja2 import ChoiceLoader, FileSystemLoader
|
|
144
|
+
|
|
130
145
|
template_overrides = [
|
|
131
146
|
e for e in registry.all_entries() if e.category == "template"
|
|
132
147
|
]
|
|
133
148
|
if not template_overrides:
|
|
134
149
|
return
|
|
135
150
|
|
|
151
|
+
app = self._app
|
|
152
|
+
folders: list[str] = []
|
|
153
|
+
seen: set[str] = set()
|
|
136
154
|
for entry in template_overrides:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
short = entry.refiner.replace("splent_feature_", "")
|
|
156
|
+
bp = app.blueprints.get(short)
|
|
157
|
+
if bp is None or not bp.template_folder:
|
|
158
|
+
continue
|
|
159
|
+
folder = os.path.join(bp.root_path, bp.template_folder)
|
|
160
|
+
if folder not in seen and os.path.isdir(folder):
|
|
161
|
+
seen.add(folder)
|
|
162
|
+
folders.append(folder)
|
|
145
163
|
logger.info(
|
|
146
164
|
"Template override: %s (from %s, by %s)",
|
|
147
165
|
entry.target,
|
|
@@ -149,6 +167,11 @@ class FeatureManager:
|
|
|
149
167
|
entry.refiner,
|
|
150
168
|
)
|
|
151
169
|
|
|
170
|
+
if folders:
|
|
171
|
+
app.jinja_env.loader = ChoiceLoader(
|
|
172
|
+
[FileSystemLoader(folders), app.jinja_env.loader]
|
|
173
|
+
)
|
|
174
|
+
|
|
152
175
|
def _setup_refinement_registry(self, product_dir: str, ordered: list[str]) -> None:
|
|
153
176
|
"""Read extensible/refinement declarations from feature pyproject files,
|
|
154
177
|
validate them, and populate the global RefinementRegistry."""
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/jinja_manager.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from flask import Flask
|
|
2
|
+
from splent_framework.assets.asset_registry import get_assets
|
|
2
3
|
from splent_framework.context.context_manager import build_jinja_context
|
|
3
4
|
from splent_framework.hooks.template_hooks import get_template_hooks
|
|
4
5
|
|
|
@@ -17,3 +18,4 @@ class JinjaManager:
|
|
|
17
18
|
|
|
18
19
|
def _register_globals(self):
|
|
19
20
|
self.app.jinja_env.globals["get_template_hooks"] = get_template_hooks
|
|
21
|
+
self.app.jinja_env.globals["get_assets"] = get_assets
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/locale_manager.py
RENAMED
|
@@ -72,9 +72,15 @@ class LocaleManager:
|
|
|
72
72
|
if translations_dir not in dirs:
|
|
73
73
|
dirs.append(translations_dir)
|
|
74
74
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
# flask-babel computes its translation directories ONCE in init_app
|
|
76
|
+
# (BabelConfiguration.translation_directories) and never re-reads the
|
|
77
|
+
# config. Features load AFTER Babel is initialised, so we (1) keep the
|
|
78
|
+
# config in sync and (2) mutate the live BabelConfiguration list
|
|
79
|
+
# (app.extensions["babel"]) so the new directory actually takes effect.
|
|
80
|
+
app.config["BABEL_TRANSLATION_DIRECTORIES"] = ";".join(dirs)
|
|
81
|
+
babel_cfg = app.extensions.get("babel")
|
|
82
|
+
live = getattr(babel_cfg, "translation_directories", None)
|
|
83
|
+
if isinstance(live, list) and translations_dir not in live:
|
|
84
|
+
live.append(translations_dir)
|
|
79
85
|
|
|
80
86
|
logger.debug("Registered translations: %s", translations_dir)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# splent_framework/nav/nav_registry.py
|
|
2
|
+
#
|
|
3
|
+
# SPL navigation registry. Each CONTENT feature declares its public main-nav
|
|
4
|
+
# entry in init_feature() via register_nav_item(); the menu is therefore
|
|
5
|
+
# *composed from the features actually selected in the product's derivation*,
|
|
6
|
+
# not hardcoded. The theme reads get_nav_items() and layers the admin "Menus"
|
|
7
|
+
# editor's runtime overrides (order / visibility / label) and custom links on
|
|
8
|
+
# top. Remove a feature (re-derive) and its entry disappears on its own; add
|
|
9
|
+
# one and it shows up — that is the SPL variability surfacing in the UI.
|
|
10
|
+
#
|
|
11
|
+
# Populated once at app startup during feature registration; read-only during
|
|
12
|
+
# request handling. Registration is idempotent by key, so a second create_app()
|
|
13
|
+
# (tests, reload) does not duplicate entries. Not thread-safe for writes — do
|
|
14
|
+
# not call register_nav_item() from request handlers.
|
|
15
|
+
|
|
16
|
+
_nav_items: dict = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_nav_item(
|
|
20
|
+
key: str, label: str, href: str, order: int = 100, icon=None
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Declare one entry in the public main navigation (opt-in, per feature).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
key: stable identifier (e.g. "projects"); used to reconcile runtime
|
|
26
|
+
overrides across re-derivations. Unique per feature.
|
|
27
|
+
label: default human label (translated at render time via gettext).
|
|
28
|
+
href: public URL/path the entry points to (e.g. "/projects").
|
|
29
|
+
order: lower sorts first when no runtime override exists.
|
|
30
|
+
icon: optional feather icon name.
|
|
31
|
+
"""
|
|
32
|
+
_nav_items[key] = {
|
|
33
|
+
"key": key,
|
|
34
|
+
"label": label,
|
|
35
|
+
"href": href,
|
|
36
|
+
"order": order,
|
|
37
|
+
"icon": icon,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_nav_items() -> list:
|
|
42
|
+
"""All declared nav entries, sorted by (order, label)."""
|
|
43
|
+
return sorted(
|
|
44
|
+
_nav_items.values(), key=lambda i: (i.get("order", 100), i.get("label", ""))
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def clear_nav_items() -> None:
|
|
49
|
+
"""Drop all entries. Intended for test teardown."""
|
|
50
|
+
_nav_items.clear()
|
|
File without changes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# splent_framework/settings/settings_schema.py
|
|
2
|
+
#
|
|
3
|
+
# SPL feature-settings schema registry — the generic way to make any feature
|
|
4
|
+
# that injects UI configurable from the admin.
|
|
5
|
+
#
|
|
6
|
+
# A feature DECLARES its admin-editable settings (a typed schema) via
|
|
7
|
+
# register_settings() in init_feature(). The `settings` feature renders ONE
|
|
8
|
+
# generic admin panel for every declared schema and persists values through
|
|
9
|
+
# SettingsService. No per-feature panel code, no ad-hoc config modules: declare
|
|
10
|
+
# the schema, get a panel + typed reads.
|
|
11
|
+
#
|
|
12
|
+
# Setting keys are namespaced as "<feature>_<field>" automatically, so features
|
|
13
|
+
# never collide. Cleared per app at feature-load (like the nav/asset registries).
|
|
14
|
+
|
|
15
|
+
_schemas: dict = {}
|
|
16
|
+
|
|
17
|
+
FIELD_TYPES = ("bool", "int", "text", "color", "select")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register_settings(
|
|
21
|
+
feature: str, title: str, fields: list, icon: str = "sliders"
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Declare a feature's admin-editable settings.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
feature: stable key, e.g. "slider" (also the panel URL segment and the
|
|
27
|
+
setting-key namespace).
|
|
28
|
+
title: human title shown in the panel/sidebar, e.g. "Slider".
|
|
29
|
+
fields: list of dicts ``{key, type, default, label, help?, options?}``.
|
|
30
|
+
``type`` in bool/int/text/color/select. For select, provide
|
|
31
|
+
``options`` as a list of ``(value, label)`` pairs.
|
|
32
|
+
icon: feather icon name for the sidebar entry.
|
|
33
|
+
"""
|
|
34
|
+
_schemas[feature] = {
|
|
35
|
+
"feature": feature,
|
|
36
|
+
"title": title,
|
|
37
|
+
"icon": icon,
|
|
38
|
+
"fields": list(fields),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_schemas() -> list:
|
|
43
|
+
"""All declared schemas, sorted by title."""
|
|
44
|
+
return sorted(_schemas.values(), key=lambda s: s["title"].lower())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_schema(feature: str):
|
|
48
|
+
return _schemas.get(feature)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def clear_settings_schemas() -> None:
|
|
52
|
+
_schemas.clear()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def setting_key(feature: str, field_key: str) -> str:
|
|
56
|
+
"""The namespaced SettingsService key for a feature field."""
|
|
57
|
+
return f"{feature}_{field_key}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _cast(field: dict, raw):
|
|
61
|
+
"""Coerce a stored raw string to the field's type, falling back to default."""
|
|
62
|
+
t = field.get("type")
|
|
63
|
+
default = field.get("default")
|
|
64
|
+
if t == "bool":
|
|
65
|
+
if raw is None:
|
|
66
|
+
return default in (True, "1", 1)
|
|
67
|
+
return raw in (True, "1", 1)
|
|
68
|
+
if t == "int":
|
|
69
|
+
try:
|
|
70
|
+
return int(raw)
|
|
71
|
+
except (TypeError, ValueError):
|
|
72
|
+
try:
|
|
73
|
+
return int(default)
|
|
74
|
+
except (TypeError, ValueError):
|
|
75
|
+
return 0
|
|
76
|
+
if t == "color":
|
|
77
|
+
import re
|
|
78
|
+
|
|
79
|
+
if raw and re.match(r"^#[0-9a-fA-F]{3,8}$", raw):
|
|
80
|
+
return raw
|
|
81
|
+
return default if default else "#000000"
|
|
82
|
+
# text / select
|
|
83
|
+
return raw if raw not in (None, "") else (default if default is not None else "")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_config(feature: str) -> dict:
|
|
87
|
+
"""Typed current values for a feature's settings, keyed by the SHORT field
|
|
88
|
+
key (e.g. {"autoplay": True, "interval": 6}). Reads SettingsService and
|
|
89
|
+
falls back to declared defaults."""
|
|
90
|
+
from splent_framework.services.service_locator import service_proxy
|
|
91
|
+
|
|
92
|
+
schema = _schemas.get(feature)
|
|
93
|
+
if not schema:
|
|
94
|
+
return {}
|
|
95
|
+
try:
|
|
96
|
+
svc = service_proxy("SettingsService")
|
|
97
|
+
except Exception:
|
|
98
|
+
svc = None
|
|
99
|
+
out = {}
|
|
100
|
+
for f in schema["fields"]:
|
|
101
|
+
raw = svc.get(setting_key(feature, f["key"]), None) if svc else None
|
|
102
|
+
out[f["key"]] = _cast(f, raw)
|
|
103
|
+
return out
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/signals/signal_utils.py
RENAMED
|
@@ -20,6 +20,11 @@ logger = logging.getLogger(__name__)
|
|
|
20
20
|
# Shared blinker namespace for all SPLENT feature signals
|
|
21
21
|
_splent_ns = Namespace()
|
|
22
22
|
|
|
23
|
+
# Handlers that connected before their producer defined the signal, keyed by
|
|
24
|
+
# signal name. Flushed by define_signal(), so feature load order doesn't matter
|
|
25
|
+
# (a captcha provider can connect to "comment-submitting" before comments loads).
|
|
26
|
+
_pending: dict = {}
|
|
27
|
+
|
|
23
28
|
|
|
24
29
|
def define_signal(name: str, provider: str) -> NamedSignal:
|
|
25
30
|
"""Define a feature signal and register it in the signal registry.
|
|
@@ -39,6 +44,10 @@ def define_signal(name: str, provider: str) -> NamedSignal:
|
|
|
39
44
|
"""
|
|
40
45
|
signal = _splent_ns.signal(name)
|
|
41
46
|
register_signal(name, signal, provider)
|
|
47
|
+
# Flush handlers that connected before this signal was defined (a listener
|
|
48
|
+
# feature that loaded before its producer). Load order is now irrelevant.
|
|
49
|
+
for fn in _pending.pop(name, []):
|
|
50
|
+
signal.connect(fn)
|
|
42
51
|
return signal
|
|
43
52
|
|
|
44
53
|
|
|
@@ -62,14 +71,17 @@ def connect_signal(signal_name: str, listener_feature: str):
|
|
|
62
71
|
"""
|
|
63
72
|
|
|
64
73
|
def decorator(fn):
|
|
74
|
+
register_listener(signal_name, listener_feature)
|
|
65
75
|
signal = get_signal(signal_name)
|
|
66
76
|
if signal is not None:
|
|
67
77
|
signal.connect(fn)
|
|
68
|
-
register_listener(signal_name, listener_feature)
|
|
69
78
|
else:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
# Producer not loaded yet (or absent). Defer the connection: if the
|
|
80
|
+
# producer's define_signal() runs later it flushes this handler. If
|
|
81
|
+
# the producer is never installed, the handler simply never fires.
|
|
82
|
+
_pending.setdefault(signal_name, []).append(fn)
|
|
83
|
+
logger.debug(
|
|
84
|
+
"Signal '%s' not defined yet — deferring handler %s.%s.",
|
|
73
85
|
signal_name,
|
|
74
86
|
fn.__module__,
|
|
75
87
|
fn.__name__,
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splent_framework
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: SPLENT-FRAMEWORK is a set of libraries for agile product development within SPLENT.
|
|
5
5
|
Author-email: DiversoLab <diversolab@us.es>
|
|
6
6
|
Project-URL: Homepage, https://github.com/diverso-lab/splent_framework
|
|
@@ -9,6 +9,10 @@ src/splent_framework.egg-info/SOURCES.txt
|
|
|
9
9
|
src/splent_framework.egg-info/dependency_links.txt
|
|
10
10
|
src/splent_framework.egg-info/requires.txt
|
|
11
11
|
src/splent_framework.egg-info/top_level.txt
|
|
12
|
+
src/splent_framework/admin/__init__.py
|
|
13
|
+
src/splent_framework/admin/registry.py
|
|
14
|
+
src/splent_framework/assets/__init__.py
|
|
15
|
+
src/splent_framework/assets/asset_registry.py
|
|
12
16
|
src/splent_framework/blueprints/__init__.py
|
|
13
17
|
src/splent_framework/blueprints/base_blueprint.py
|
|
14
18
|
src/splent_framework/bootstraps/__init__.py
|
|
@@ -47,6 +51,8 @@ src/splent_framework/managers/task_queue_manager.py
|
|
|
47
51
|
src/splent_framework/migrations/__init__.py
|
|
48
52
|
src/splent_framework/migrations/feature_env.py
|
|
49
53
|
src/splent_framework/namespaces/__init__.py
|
|
54
|
+
src/splent_framework/nav/__init__.py
|
|
55
|
+
src/splent_framework/nav/nav_registry.py
|
|
50
56
|
src/splent_framework/refinement/__init__.py
|
|
51
57
|
src/splent_framework/refinement/model_extender.py
|
|
52
58
|
src/splent_framework/refinement/parser.py
|
|
@@ -65,6 +71,8 @@ src/splent_framework/serialisers/serializer.py
|
|
|
65
71
|
src/splent_framework/services/BaseService.py
|
|
66
72
|
src/splent_framework/services/__init__.py
|
|
67
73
|
src/splent_framework/services/service_locator.py
|
|
74
|
+
src/splent_framework/settings/__init__.py
|
|
75
|
+
src/splent_framework/settings/settings_schema.py
|
|
68
76
|
src/splent_framework/signals/__init__.py
|
|
69
77
|
src/splent_framework/signals/registry.py
|
|
70
78
|
src/splent_framework/signals/signal_utils.py
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# splent_framework/template_hooks.py
|
|
2
|
-
#
|
|
3
|
-
# NOTE: _hooks is a module-level registry populated once at app startup
|
|
4
|
-
# (during feature registration) and read-only during request handling.
|
|
5
|
-
# It is NOT thread-safe for concurrent writes; do not call
|
|
6
|
-
# register_template_hook() from request handlers or background threads.
|
|
7
|
-
|
|
8
|
-
_hooks: dict[str, list] = {}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def register_template_hook(name: str, func) -> None:
|
|
12
|
-
"""Append a callback to a named hook slot (additive)."""
|
|
13
|
-
_hooks.setdefault(name, []).append(func)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def replace_template_hook(name: str, func) -> None:
|
|
17
|
-
"""Replace ALL existing callbacks for a named hook slot.
|
|
18
|
-
|
|
19
|
-
Used by refinement features to override a base feature's hook content.
|
|
20
|
-
"""
|
|
21
|
-
_hooks[name] = [func]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def remove_template_hook(name: str, func) -> None:
|
|
25
|
-
"""Remove a specific callback from a hook slot."""
|
|
26
|
-
if name in _hooks:
|
|
27
|
-
_hooks[name] = [f for f in _hooks[name] if f is not func]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def get_template_hooks(name: str) -> list:
|
|
31
|
-
return _hooks.get(name, [])
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def clear_hooks() -> None:
|
|
35
|
-
"""Remove all registered hooks. Intended for use in test teardown."""
|
|
36
|
-
_hooks.clear()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/blueprints/base_blueprint.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/context/context_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/decorators/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/helpers/test_helpers_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/config_manager.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/db_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/feature_order.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/logging_manager.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/migration_manager.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/namespace_manager.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/managers/session_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/migrations/feature_env.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/__init__.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/model_extender.py
RENAMED
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/registry.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/refinement/validator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/resources/generic_resource.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/seeders/BaseSeeder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/serialisers/serializer.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/services/BaseService.py
RENAMED
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/services/service_locator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/utils/feature_utils.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/utils/form_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework/utils/pyproject_reader.py
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework.egg-info/requires.txt
RENAMED
|
File without changes
|
{splent_framework-1.6.2 → splent_framework-1.7.0}/src/splent_framework.egg-info/top_level.txt
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
|