splent-framework 0.0.3__tar.gz → 1.2.5__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.2.5/PKG-INFO +64 -0
- splent_framework-1.2.5/README.md +36 -0
- {splent_framework-0.0.3 → splent_framework-1.2.5}/pyproject.toml +13 -2
- splent_framework-1.2.5/src/splent_framework/blueprints/base_blueprint.py +87 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/bootstraps/locustfile_bootstrap.py +9 -6
- splent_framework-1.2.5/src/splent_framework/configuration/default_config.py +56 -0
- splent_framework-1.2.5/src/splent_framework/context/context_manager.py +26 -0
- splent_framework-1.2.5/src/splent_framework/db.py +3 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/decorators/decorators.py +0 -2
- splent_framework-1.2.5/src/splent_framework/fixtures/fixtures.py +69 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/helpers/test_helpers_auth.py +1 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/helpers/test_helpers_db.py +1 -2
- splent_framework-1.2.5/src/splent_framework/hooks/template_hooks.py +21 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/locust/common.py +6 -4
- splent_framework-1.2.5/src/splent_framework/managers/config_manager.py +46 -0
- splent_framework-1.2.5/src/splent_framework/managers/db_manager.py +15 -0
- splent_framework-1.2.5/src/splent_framework/managers/error_handler_manager.py +60 -0
- splent_framework-1.2.5/src/splent_framework/managers/feature_loader.py +365 -0
- splent_framework-1.2.5/src/splent_framework/managers/feature_manager.py +117 -0
- splent_framework-1.2.5/src/splent_framework/managers/feature_order.py +227 -0
- splent_framework-1.2.5/src/splent_framework/managers/jinja_manager.py +19 -0
- splent_framework-1.2.5/src/splent_framework/managers/logging_manager.py +42 -0
- splent_framework-1.2.5/src/splent_framework/managers/migration_manager.py +236 -0
- splent_framework-1.2.5/src/splent_framework/managers/namespace_manager.py +91 -0
- splent_framework-1.2.5/src/splent_framework/managers/session_manager.py +7 -0
- splent_framework-1.2.5/src/splent_framework/managers/task_queue_manager.py +51 -0
- splent_framework-1.2.5/src/splent_framework/migrations/feature_env.py +93 -0
- splent_framework-1.2.5/src/splent_framework/repositories/BaseRepository.py +65 -0
- splent_framework-1.2.5/src/splent_framework/resources/generic_resource.py +96 -0
- splent_framework-1.2.5/src/splent_framework/seeders/BaseSeeder.py +45 -0
- splent_framework-1.2.5/src/splent_framework/seeders/__init__.py +0 -0
- splent_framework-1.2.5/src/splent_framework/selenium/__init__.py +0 -0
- splent_framework-1.2.5/src/splent_framework/serialisers/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/serialisers/serializer.py +17 -10
- splent_framework-1.2.5/src/splent_framework/services/BaseService.py +44 -0
- splent_framework-1.2.5/src/splent_framework/services/__init__.py +0 -0
- splent_framework-1.2.5/src/splent_framework/utils/__init__.py +0 -0
- splent_framework-1.2.5/src/splent_framework/utils/app_loader.py +35 -0
- splent_framework-1.2.5/src/splent_framework/utils/feature_utils.py +25 -0
- splent_framework-1.2.5/src/splent_framework/utils/path_utils.py +71 -0
- splent_framework-1.2.5/src/splent_framework/utils/pyproject_reader.py +161 -0
- splent_framework-1.2.5/src/splent_framework.egg-info/PKG-INFO +64 -0
- splent_framework-1.2.5/src/splent_framework.egg-info/SOURCES.txt +80 -0
- {splent_framework-0.0.3 → splent_framework-1.2.5}/src/splent_framework.egg-info/requires.txt +4 -0
- splent_framework-1.2.5/tests/test_base_blueprint_security.py +149 -0
- splent_framework-1.2.5/tests/test_base_repository.py +168 -0
- splent_framework-1.2.5/tests/test_config_manager.py +76 -0
- splent_framework-1.2.5/tests/test_config_manager_return.py +26 -0
- splent_framework-1.2.5/tests/test_context_manager.py +88 -0
- splent_framework-1.2.5/tests/test_default_config.py +140 -0
- splent_framework-1.2.5/tests/test_error_handler_manager.py +109 -0
- splent_framework-1.2.5/tests/test_feature_manager_parse.py +82 -0
- splent_framework-1.2.5/tests/test_feature_order.py +233 -0
- splent_framework-1.2.5/tests/test_feature_utils.py +73 -0
- splent_framework-1.2.5/tests/test_generic_resource.py +172 -0
- splent_framework-1.2.5/tests/test_migration_manager.py +204 -0
- splent_framework-1.2.5/tests/test_namespace_manager.py +91 -0
- splent_framework-1.2.5/tests/test_pyproject_reader.py +176 -0
- splent_framework-1.2.5/tests/test_pyproject_reader_env.py +62 -0
- splent_framework-0.0.3/PKG-INFO +0 -26
- splent_framework-0.0.3/README.md +0 -1
- splent_framework-0.0.3/src/splent_framework/core/blueprints/base_blueprint.py +0 -72
- splent_framework-0.0.3/src/splent_framework/core/managers/config_manager.py +0 -65
- splent_framework-0.0.3/src/splent_framework/core/managers/error_handler_manager.py +0 -27
- splent_framework-0.0.3/src/splent_framework/core/managers/feature_manager.py +0 -72
- splent_framework-0.0.3/src/splent_framework/core/managers/logging_manager.py +0 -33
- splent_framework-0.0.3/src/splent_framework/core/managers/task_queue_manager.py +0 -52
- splent_framework-0.0.3/src/splent_framework/core/repositories/BaseRepository.py +0 -63
- splent_framework-0.0.3/src/splent_framework/core/resources/generic_resource.py +0 -81
- splent_framework-0.0.3/src/splent_framework/core/seeders/BaseSeeder.py +0 -60
- splent_framework-0.0.3/src/splent_framework/core/services/BaseService.py +0 -42
- splent_framework-0.0.3/src/splent_framework.egg-info/PKG-INFO +0 -26
- splent_framework-0.0.3/src/splent_framework.egg-info/SOURCES.txt +0 -43
- {splent_framework-0.0.3 → splent_framework-1.2.5}/LICENSE +0 -0
- {splent_framework-0.0.3 → splent_framework-1.2.5}/setup.cfg +0 -0
- {splent_framework-0.0.3 → splent_framework-1.2.5}/src/splent_framework/_init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework/blueprints}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/blueprints → splent_framework-1.2.5/src/splent_framework/bootstraps}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/bootstraps → splent_framework-1.2.5/src/splent_framework/configuration}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/configuration/configuration.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/configuration → splent_framework-1.2.5/src/splent_framework/context}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/decorators/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/environment/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/environment/host.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/helpers → splent_framework-1.2.5/src/splent_framework/fixtures}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/locust → splent_framework-1.2.5/src/splent_framework/helpers}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/managers → splent_framework-1.2.5/src/splent_framework/hooks}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/repositories → splent_framework-1.2.5/src/splent_framework/locust}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/resources → splent_framework-1.2.5/src/splent_framework/managers}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/seeders → splent_framework-1.2.5/src/splent_framework/migrations}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/selenium → splent_framework-1.2.5/src/splent_framework/namespaces}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/serialisers → splent_framework-1.2.5/src/splent_framework/repositories}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core/services → splent_framework-1.2.5/src/splent_framework/resources}/__init__.py +0 -0
- {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/selenium/common.py +0 -0
- {splent_framework-0.0.3 → splent_framework-1.2.5}/src/splent_framework.egg-info/dependency_links.txt +0 -0
- {splent_framework-0.0.3 → splent_framework-1.2.5}/src/splent_framework.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: splent_framework
|
|
3
|
+
Version: 1.2.5
|
|
4
|
+
Summary: SPLENT-FRAMEWORK is a set of libraries for agile product development within SPLENT.
|
|
5
|
+
Author-email: DiversoLab <diversolab@us.es>
|
|
6
|
+
Project-URL: Homepage, https://github.com/diverso-lab/splent_framework
|
|
7
|
+
Project-URL: Issues, https://github.com/diverso-lab/splent_framework/issues
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: python-dotenv==1.0.1
|
|
12
|
+
Requires-Dist: sqlalchemy==2.0.37
|
|
13
|
+
Requires-Dist: flask==3.1.0
|
|
14
|
+
Requires-Dist: flask_sqlalchemy==3.1.1
|
|
15
|
+
Requires-Dist: flask_migrate==4.1.0
|
|
16
|
+
Requires-Dist: flask_session==0.8.0
|
|
17
|
+
Requires-Dist: flask_mail==0.10.0
|
|
18
|
+
Requires-Dist: flask_login==0.6.3
|
|
19
|
+
Requires-Dist: flask_wtf==1.2.2
|
|
20
|
+
Requires-Dist: redis==5.2.1
|
|
21
|
+
Requires-Dist: pymysql==1.1.1
|
|
22
|
+
Requires-Dist: psutil==6.1.1
|
|
23
|
+
Requires-Dist: pytz==2024.2
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=6.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# SPLENT Framework
|
|
30
|
+
|
|
31
|
+
Core library for building modular, feature-driven Flask applications using Software Product Line principles.
|
|
32
|
+
|
|
33
|
+
## What it provides
|
|
34
|
+
|
|
35
|
+
- **Manager pattern** — Pluggable subsystem managers (features, config, migrations, sessions, Jinja, error handling)
|
|
36
|
+
- **Feature system** — Modular features with lifecycle tracking, UVL-based dependency ordering, and per-feature Alembic migrations
|
|
37
|
+
- **Base classes** — `BaseBlueprint`, `BaseRepository`, `BaseService`, `BaseSeeder`, `GenericResource`
|
|
38
|
+
- **App factory** — `create_app()` wires everything together with env-aware feature loading
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from splent_framework.managers.feature_manager import FeatureManager
|
|
44
|
+
|
|
45
|
+
def create_app(config_name="development"):
|
|
46
|
+
app = Flask(__name__)
|
|
47
|
+
# ... config, db, sessions ...
|
|
48
|
+
FeatureManager(app, strict=False).register_features()
|
|
49
|
+
return app
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- Python 3.13+
|
|
55
|
+
- Flask 3.1+
|
|
56
|
+
- SQLAlchemy 2.0+
|
|
57
|
+
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
Full documentation at **[docs.splent.io](https://docs.splent.io)**
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
Creative Commons CC BY 4.0 - SPLENT - Diverso Lab
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPLENT Framework
|
|
2
|
+
|
|
3
|
+
Core library for building modular, feature-driven Flask applications using Software Product Line principles.
|
|
4
|
+
|
|
5
|
+
## What it provides
|
|
6
|
+
|
|
7
|
+
- **Manager pattern** — Pluggable subsystem managers (features, config, migrations, sessions, Jinja, error handling)
|
|
8
|
+
- **Feature system** — Modular features with lifecycle tracking, UVL-based dependency ordering, and per-feature Alembic migrations
|
|
9
|
+
- **Base classes** — `BaseBlueprint`, `BaseRepository`, `BaseService`, `BaseSeeder`, `GenericResource`
|
|
10
|
+
- **App factory** — `create_app()` wires everything together with env-aware feature loading
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from splent_framework.managers.feature_manager import FeatureManager
|
|
16
|
+
|
|
17
|
+
def create_app(config_name="development"):
|
|
18
|
+
app = Flask(__name__)
|
|
19
|
+
# ... config, db, sessions ...
|
|
20
|
+
FeatureManager(app, strict=False).register_features()
|
|
21
|
+
return app
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Python 3.13+
|
|
27
|
+
- Flask 3.1+
|
|
28
|
+
- SQLAlchemy 2.0+
|
|
29
|
+
|
|
30
|
+
## Documentation
|
|
31
|
+
|
|
32
|
+
Full documentation at **[docs.splent.io](https://docs.splent.io)**
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
Creative Commons CC BY 4.0 - SPLENT - Diverso Lab
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "splent_framework"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.2.5"
|
|
8
8
|
description = "SPLENT-FRAMEWORK is a set of libraries for agile product development within SPLENT."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
11
|
authors = [{ name = "DiversoLab", email = "diversolab@us.es" }]
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
|
|
@@ -27,12 +27,23 @@ dependencies = [
|
|
|
27
27
|
"pytz==2024.2"
|
|
28
28
|
]
|
|
29
29
|
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.3",
|
|
33
|
+
"pytest-cov>=6.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
30
36
|
[tool.setuptools]
|
|
31
37
|
package-dir = { "" = "src" }
|
|
32
38
|
|
|
33
39
|
[tool.setuptools.packages.find]
|
|
34
40
|
where = ["src"]
|
|
35
41
|
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
pythonpath = ["src"]
|
|
45
|
+
filterwarnings = ["ignore::pytest.PytestCollectionWarning"]
|
|
46
|
+
|
|
36
47
|
[project.urls]
|
|
37
48
|
Homepage = "https://github.com/diverso-lab/splent_framework"
|
|
38
49
|
Issues = "https://github.com/diverso-lab/splent_framework/issues"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# base_blueprint.py
|
|
2
|
+
from flask import Blueprint, Response, abort
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import importlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseBlueprint(Blueprint):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
name,
|
|
12
|
+
import_name, # typically __name__ of the feature package
|
|
13
|
+
static_folder=None,
|
|
14
|
+
static_url_path=None,
|
|
15
|
+
template_folder=None,
|
|
16
|
+
url_prefix=None,
|
|
17
|
+
subdomain=None,
|
|
18
|
+
url_defaults=None,
|
|
19
|
+
root_path=None,
|
|
20
|
+
):
|
|
21
|
+
super().__init__(
|
|
22
|
+
name,
|
|
23
|
+
import_name,
|
|
24
|
+
static_folder=static_folder,
|
|
25
|
+
static_url_path=static_url_path,
|
|
26
|
+
template_folder=template_folder,
|
|
27
|
+
url_prefix=url_prefix,
|
|
28
|
+
subdomain=subdomain,
|
|
29
|
+
url_defaults=url_defaults,
|
|
30
|
+
root_path=root_path,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Resolve feature package directory via Python's module system
|
|
34
|
+
try:
|
|
35
|
+
module = sys.modules.get(import_name) or importlib.import_module(
|
|
36
|
+
import_name
|
|
37
|
+
)
|
|
38
|
+
pkg_dir = os.path.dirname(module.__file__)
|
|
39
|
+
except (ImportError, AttributeError) as e:
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
f"Cannot resolve package path for {import_name}: {e}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Root directory of this feature package (src/splent_io/splent_feature_xxx)
|
|
45
|
+
self.feature_code_path = pkg_dir
|
|
46
|
+
|
|
47
|
+
# Fall back to the feature's own templates/ if none was passed
|
|
48
|
+
if self.template_folder is None:
|
|
49
|
+
self.template_folder = os.path.join(self.feature_code_path, "templates")
|
|
50
|
+
|
|
51
|
+
self.add_asset_routes()
|
|
52
|
+
|
|
53
|
+
def add_asset_routes(self):
|
|
54
|
+
assets_folder = os.path.join(self.feature_code_path, "assets")
|
|
55
|
+
if os.path.exists(assets_folder):
|
|
56
|
+
self.add_url_rule(
|
|
57
|
+
f"/{self.name}/<path:subfolder>/<path:filename>",
|
|
58
|
+
"assets",
|
|
59
|
+
self.send_file,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def send_file(self, subfolder, filename):
|
|
63
|
+
allowed_subfolders = {"js", "css", "dist"}
|
|
64
|
+
if subfolder not in allowed_subfolders:
|
|
65
|
+
abort(404)
|
|
66
|
+
|
|
67
|
+
base_dir = os.path.realpath(
|
|
68
|
+
os.path.join(self.feature_code_path, "assets", subfolder)
|
|
69
|
+
)
|
|
70
|
+
requested_path = os.path.realpath(os.path.join(base_dir, filename))
|
|
71
|
+
|
|
72
|
+
# Prevent path traversal: resolved path must stay inside base_dir
|
|
73
|
+
if not requested_path.startswith(base_dir + os.sep) and requested_path != base_dir:
|
|
74
|
+
abort(403)
|
|
75
|
+
|
|
76
|
+
if not os.path.isfile(requested_path):
|
|
77
|
+
abort(404)
|
|
78
|
+
|
|
79
|
+
if filename.endswith(".js"):
|
|
80
|
+
mimetype = "application/javascript"
|
|
81
|
+
elif filename.endswith(".css"):
|
|
82
|
+
mimetype = "text/css"
|
|
83
|
+
else:
|
|
84
|
+
mimetype = "text/plain"
|
|
85
|
+
|
|
86
|
+
with open(requested_path, "r") as f:
|
|
87
|
+
return Response(f.read(), mimetype=mimetype)
|
|
@@ -2,27 +2,30 @@ import os
|
|
|
2
2
|
import glob
|
|
3
3
|
import inspect
|
|
4
4
|
import importlib.util
|
|
5
|
+
import logging
|
|
5
6
|
from locust import HttpUser
|
|
6
7
|
from dotenv import load_dotenv
|
|
7
8
|
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
def load_locustfiles():
|
|
10
13
|
load_dotenv()
|
|
11
14
|
working_dir = os.getenv("WORKING_DIR", "")
|
|
12
|
-
|
|
15
|
+
logger.debug("Working directory: %s", working_dir)
|
|
13
16
|
|
|
14
17
|
module_dir = os.path.join(working_dir, "app", "modules")
|
|
15
|
-
|
|
18
|
+
logger.debug("Module directory: %s", module_dir)
|
|
16
19
|
|
|
17
20
|
locustfile_paths = glob.glob(
|
|
18
21
|
os.path.join(module_dir, "*", "tests", "locustfile.py")
|
|
19
22
|
)
|
|
20
|
-
|
|
23
|
+
logger.debug("Found locustfiles: %s", locustfile_paths)
|
|
21
24
|
|
|
22
25
|
found_user_classes = []
|
|
23
26
|
|
|
24
27
|
for path in locustfile_paths:
|
|
25
|
-
|
|
28
|
+
logger.debug("Loading locustfile: %s", path)
|
|
26
29
|
module_name = os.path.splitext(os.path.basename(path))[0]
|
|
27
30
|
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
28
31
|
locustfile = importlib.util.module_from_spec(spec)
|
|
@@ -38,7 +41,7 @@ def load_locustfiles():
|
|
|
38
41
|
unique_name = f"{name}_{os.path.basename(path).split('.')[0]}"
|
|
39
42
|
globals()[unique_name] = obj # Add to globals
|
|
40
43
|
found_user_classes.append((unique_name, obj))
|
|
41
|
-
|
|
44
|
+
logger.debug("Loaded user class: %s", unique_name)
|
|
42
45
|
|
|
43
46
|
if not found_user_classes:
|
|
44
47
|
raise ValueError("No User class found!")
|
|
@@ -47,4 +50,4 @@ def load_locustfiles():
|
|
|
47
50
|
|
|
48
51
|
|
|
49
52
|
found_user_classes = load_locustfiles()
|
|
50
|
-
|
|
53
|
+
logger.info("Total user classes loaded: %d", len(found_user_classes))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _build_db_uri(db_name_env: str, db_name_default: str) -> str:
|
|
5
|
+
return (
|
|
6
|
+
f"mysql+pymysql://{os.getenv('MARIADB_USER', 'default_user')}:"
|
|
7
|
+
f"{os.getenv('MARIADB_PASSWORD', 'default_password')}@"
|
|
8
|
+
f"{os.getenv('MARIADB_HOSTNAME', 'localhost')}:3306/"
|
|
9
|
+
f"{os.getenv(db_name_env, db_name_default)}"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Config:
|
|
14
|
+
# WARNING: override SECRET_KEY via env var in all non-development environments
|
|
15
|
+
SECRET_KEY = os.getenv("SECRET_KEY", "dev_test_key_1234567890abcdefghijklmnopqrstu")
|
|
16
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
17
|
+
TEMPLATES_AUTO_RELOAD = True
|
|
18
|
+
UPLOAD_FOLDER = "uploads"
|
|
19
|
+
SESSION_TYPE = "filesystem"
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
# Resolved at instantiation time so runtime env changes are picked up
|
|
23
|
+
self.TIMEZONE = os.getenv("TIMEZONE", "Europe/Madrid")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DevelopmentConfig(Config):
|
|
27
|
+
DEBUG = True
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.SQLALCHEMY_DATABASE_URI = _build_db_uri("MARIADB_DATABASE", "default_db")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestingConfig(Config):
|
|
35
|
+
TESTING = True
|
|
36
|
+
WTF_CSRF_ENABLED = False
|
|
37
|
+
SESSION_TYPE = "filesystem"
|
|
38
|
+
SESSION_FILE_DIR = "/tmp/flask_sessions"
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.SQLALCHEMY_DATABASE_URI = _build_db_uri(
|
|
43
|
+
"MARIADB_TEST_DATABASE", "default_test_db"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ProductionConfig(Config):
|
|
48
|
+
DEBUG = False
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
super().__init__()
|
|
52
|
+
if not os.getenv("SECRET_KEY"):
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
"SECRET_KEY environment variable must be set in production."
|
|
55
|
+
)
|
|
56
|
+
self.SQLALCHEMY_DATABASE_URI = _build_db_uri("MARIADB_DATABASE", "default_db")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_jinja_context(app, base_context: dict) -> dict:
|
|
7
|
+
"""
|
|
8
|
+
Combines the base context with variables injected by feature context processors.
|
|
9
|
+
"""
|
|
10
|
+
ctx = dict(base_context) # defensive copy — do not mutate the caller's dict
|
|
11
|
+
|
|
12
|
+
for fn in getattr(app, "context_processors", []):
|
|
13
|
+
try:
|
|
14
|
+
result = fn(app)
|
|
15
|
+
if isinstance(result, dict):
|
|
16
|
+
ctx.update(result)
|
|
17
|
+
else:
|
|
18
|
+
logger.warning(
|
|
19
|
+
"Feature context processor %s returned %s instead of dict",
|
|
20
|
+
fn,
|
|
21
|
+
type(result),
|
|
22
|
+
)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
logger.exception("Error in feature context processor %s: %s", fn, e)
|
|
25
|
+
|
|
26
|
+
return ctx
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from splent_framework.db import db
|
|
3
|
+
from splent_framework.utils.app_loader import get_create_app_in_testing_mode
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture(scope="session")
|
|
7
|
+
def test_app():
|
|
8
|
+
"""
|
|
9
|
+
Creates and initializes the Flask app for the test session.
|
|
10
|
+
Drops and creates all tables once per session.
|
|
11
|
+
"""
|
|
12
|
+
app = get_create_app_in_testing_mode()
|
|
13
|
+
with app.app_context():
|
|
14
|
+
db.drop_all()
|
|
15
|
+
db.create_all()
|
|
16
|
+
yield app
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="function")
|
|
20
|
+
def test_client(test_app):
|
|
21
|
+
"""
|
|
22
|
+
Provides a test client and resets the DB before and after each test.
|
|
23
|
+
This ensures test isolation.
|
|
24
|
+
"""
|
|
25
|
+
with test_app.app_context():
|
|
26
|
+
db.session.remove()
|
|
27
|
+
db.drop_all()
|
|
28
|
+
db.create_all()
|
|
29
|
+
with test_app.test_client() as client:
|
|
30
|
+
yield client
|
|
31
|
+
with test_app.app_context():
|
|
32
|
+
db.session.remove()
|
|
33
|
+
db.drop_all()
|
|
34
|
+
db.create_all()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture(scope="module")
|
|
38
|
+
def test_client_module(test_app):
|
|
39
|
+
"""
|
|
40
|
+
Shared test client for the entire module.
|
|
41
|
+
Resets the database only at the start and end of the module.
|
|
42
|
+
"""
|
|
43
|
+
with test_app.app_context():
|
|
44
|
+
db.session.remove()
|
|
45
|
+
db.drop_all()
|
|
46
|
+
db.create_all()
|
|
47
|
+
|
|
48
|
+
with test_app.test_client() as client:
|
|
49
|
+
yield client
|
|
50
|
+
|
|
51
|
+
db.session.remove()
|
|
52
|
+
db.drop_all()
|
|
53
|
+
db.create_all()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture(scope="function")
|
|
57
|
+
def clean_database(test_app):
|
|
58
|
+
"""
|
|
59
|
+
Provides a manual DB reset during a test when needed.
|
|
60
|
+
Useful if you want to control when the DB is cleaned.
|
|
61
|
+
"""
|
|
62
|
+
with test_app.app_context():
|
|
63
|
+
db.session.remove()
|
|
64
|
+
db.drop_all()
|
|
65
|
+
db.create_all()
|
|
66
|
+
yield
|
|
67
|
+
db.session.remove()
|
|
68
|
+
db.drop_all()
|
|
69
|
+
db.create_all()
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
_hooks.setdefault(name, []).append(func)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_template_hooks(name: str) -> list:
|
|
16
|
+
return _hooks.get(name, [])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def clear_hooks() -> None:
|
|
20
|
+
"""Remove all registered hooks. Intended for use in test teardown."""
|
|
21
|
+
_hooks.clear()
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from faker import Faker
|
|
2
4
|
from bs4 import BeautifulSoup
|
|
3
5
|
|
|
4
6
|
fake = Faker()
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
def get_csrf_token(response):
|
|
8
11
|
soup = BeautifulSoup(response.text, "html.parser")
|
|
9
12
|
token_tag = soup.find("input", {"name": "csrf_token"})
|
|
10
|
-
if token_tag:
|
|
11
|
-
|
|
12
|
-
else:
|
|
13
|
-
print("Response HTML:", response.text)
|
|
13
|
+
if token_tag is None:
|
|
14
|
+
logger.debug("CSRF token not found. Response HTML: %s", response.text)
|
|
14
15
|
raise ValueError("CSRF token not found in the response")
|
|
16
|
+
return token_tag["value"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigManager:
|
|
9
|
+
def __init__(self, app):
|
|
10
|
+
self.app = app
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def init_app(cls, app, config_name="development"):
|
|
14
|
+
"""Factory method to initialize and load configuration."""
|
|
15
|
+
manager = cls(app)
|
|
16
|
+
manager.load_config(config_name)
|
|
17
|
+
return manager
|
|
18
|
+
|
|
19
|
+
def load_config(self, config_name: str = "development") -> None:
|
|
20
|
+
config_name = config_name or os.getenv("FLASK_ENV", "development")
|
|
21
|
+
splent_app = os.getenv("SPLENT_APP", "splent_app")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
config_module = importlib.import_module(f"{splent_app}.config")
|
|
25
|
+
except ModuleNotFoundError:
|
|
26
|
+
from splent_framework.configuration import default_config as config_module
|
|
27
|
+
|
|
28
|
+
logger.warning("No product config.py found for '%s', using SPLENT default config.", splent_app)
|
|
29
|
+
|
|
30
|
+
config_class_name = f"{config_name.capitalize()}Config"
|
|
31
|
+
config_class = getattr(config_module, config_class_name, None)
|
|
32
|
+
|
|
33
|
+
if config_class is None:
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
f"Could not find class '{config_class_name}' in '{splent_app}.config'"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
config_instance = config_class()
|
|
39
|
+
|
|
40
|
+
# Combine instance attributes (set in __init__) with class-level uppercase attrs
|
|
41
|
+
config_data = {k: v for k, v in config_instance.__dict__.items() if k.isupper()}
|
|
42
|
+
for k in dir(config_instance):
|
|
43
|
+
if k.isupper() and k not in config_data:
|
|
44
|
+
config_data[k] = getattr(config_instance, k)
|
|
45
|
+
|
|
46
|
+
self.app.config.from_mapping(config_data)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from splent_framework.managers.migration_manager import MigrationManager
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MigrateManager(MigrationManager):
|
|
5
|
+
"""
|
|
6
|
+
Backward-compatible alias for MigrationManager.
|
|
7
|
+
|
|
8
|
+
App factories that import MigrateManager continue to work without changes:
|
|
9
|
+
|
|
10
|
+
from splent_framework.managers.db_manager import MigrateManager
|
|
11
|
+
MigrateManager(app)
|
|
12
|
+
|
|
13
|
+
All migration logic — including splent_migrations table creation and
|
|
14
|
+
per-feature migration support — is now in MigrationManager.
|
|
15
|
+
"""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import importlib
|
|
3
|
+
import types
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from flask import render_template
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorHandlerManager:
|
|
9
|
+
def __init__(self, app) -> None:
|
|
10
|
+
self.app = app
|
|
11
|
+
self.custom_handlers = self._import_custom_handlers()
|
|
12
|
+
|
|
13
|
+
def _import_custom_handlers(self) -> types.ModuleType | None:
|
|
14
|
+
splent_app = os.getenv("SPLENT_APP", "splent_app")
|
|
15
|
+
try:
|
|
16
|
+
return importlib.import_module(f"{splent_app}.errors")
|
|
17
|
+
except ModuleNotFoundError:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
def _get_handler(self, name: str, fallback: Callable) -> Callable:
|
|
21
|
+
if self.custom_handlers and hasattr(self.custom_handlers, name):
|
|
22
|
+
return getattr(self.custom_handlers, name)
|
|
23
|
+
return fallback
|
|
24
|
+
|
|
25
|
+
def register_error_handlers(self) -> None:
|
|
26
|
+
@self.app.errorhandler(500)
|
|
27
|
+
def internal_error(e):
|
|
28
|
+
return self._get_handler("handle_500", self._default_500)(self.app, e)
|
|
29
|
+
|
|
30
|
+
@self.app.errorhandler(404)
|
|
31
|
+
def not_found_error(e):
|
|
32
|
+
return self._get_handler("handle_404", self._default_404)(self.app, e)
|
|
33
|
+
|
|
34
|
+
@self.app.errorhandler(401)
|
|
35
|
+
def unauthorized_error(e):
|
|
36
|
+
return self._get_handler("handle_401", self._default_401)(self.app, e)
|
|
37
|
+
|
|
38
|
+
@self.app.errorhandler(400)
|
|
39
|
+
def bad_request_error(e):
|
|
40
|
+
return self._get_handler("handle_400", self._default_400)(self.app, e)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _default_500(app, e):
|
|
44
|
+
app.logger.error("Internal Server Error: %s", str(e))
|
|
45
|
+
return render_template("500.html"), 500
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _default_404(app, e):
|
|
49
|
+
app.logger.warning("Page Not Found: %s", str(e))
|
|
50
|
+
return render_template("404.html"), 404
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _default_401(app, e):
|
|
54
|
+
app.logger.warning("Unauthorized Access: %s", str(e))
|
|
55
|
+
return render_template("401.html"), 401
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _default_400(app, e):
|
|
59
|
+
app.logger.warning("Bad Request: %s", str(e))
|
|
60
|
+
return render_template("400.html"), 400
|