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.
Files changed (96) hide show
  1. splent_framework-1.2.5/PKG-INFO +64 -0
  2. splent_framework-1.2.5/README.md +36 -0
  3. {splent_framework-0.0.3 → splent_framework-1.2.5}/pyproject.toml +13 -2
  4. splent_framework-1.2.5/src/splent_framework/blueprints/base_blueprint.py +87 -0
  5. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/bootstraps/locustfile_bootstrap.py +9 -6
  6. splent_framework-1.2.5/src/splent_framework/configuration/default_config.py +56 -0
  7. splent_framework-1.2.5/src/splent_framework/context/context_manager.py +26 -0
  8. splent_framework-1.2.5/src/splent_framework/db.py +3 -0
  9. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/decorators/decorators.py +0 -2
  10. splent_framework-1.2.5/src/splent_framework/fixtures/fixtures.py +69 -0
  11. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/helpers/test_helpers_auth.py +1 -0
  12. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/helpers/test_helpers_db.py +1 -2
  13. splent_framework-1.2.5/src/splent_framework/hooks/template_hooks.py +21 -0
  14. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/locust/common.py +6 -4
  15. splent_framework-1.2.5/src/splent_framework/managers/config_manager.py +46 -0
  16. splent_framework-1.2.5/src/splent_framework/managers/db_manager.py +15 -0
  17. splent_framework-1.2.5/src/splent_framework/managers/error_handler_manager.py +60 -0
  18. splent_framework-1.2.5/src/splent_framework/managers/feature_loader.py +365 -0
  19. splent_framework-1.2.5/src/splent_framework/managers/feature_manager.py +117 -0
  20. splent_framework-1.2.5/src/splent_framework/managers/feature_order.py +227 -0
  21. splent_framework-1.2.5/src/splent_framework/managers/jinja_manager.py +19 -0
  22. splent_framework-1.2.5/src/splent_framework/managers/logging_manager.py +42 -0
  23. splent_framework-1.2.5/src/splent_framework/managers/migration_manager.py +236 -0
  24. splent_framework-1.2.5/src/splent_framework/managers/namespace_manager.py +91 -0
  25. splent_framework-1.2.5/src/splent_framework/managers/session_manager.py +7 -0
  26. splent_framework-1.2.5/src/splent_framework/managers/task_queue_manager.py +51 -0
  27. splent_framework-1.2.5/src/splent_framework/migrations/feature_env.py +93 -0
  28. splent_framework-1.2.5/src/splent_framework/repositories/BaseRepository.py +65 -0
  29. splent_framework-1.2.5/src/splent_framework/resources/generic_resource.py +96 -0
  30. splent_framework-1.2.5/src/splent_framework/seeders/BaseSeeder.py +45 -0
  31. splent_framework-1.2.5/src/splent_framework/seeders/__init__.py +0 -0
  32. splent_framework-1.2.5/src/splent_framework/selenium/__init__.py +0 -0
  33. splent_framework-1.2.5/src/splent_framework/serialisers/__init__.py +0 -0
  34. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/serialisers/serializer.py +17 -10
  35. splent_framework-1.2.5/src/splent_framework/services/BaseService.py +44 -0
  36. splent_framework-1.2.5/src/splent_framework/services/__init__.py +0 -0
  37. splent_framework-1.2.5/src/splent_framework/utils/__init__.py +0 -0
  38. splent_framework-1.2.5/src/splent_framework/utils/app_loader.py +35 -0
  39. splent_framework-1.2.5/src/splent_framework/utils/feature_utils.py +25 -0
  40. splent_framework-1.2.5/src/splent_framework/utils/path_utils.py +71 -0
  41. splent_framework-1.2.5/src/splent_framework/utils/pyproject_reader.py +161 -0
  42. splent_framework-1.2.5/src/splent_framework.egg-info/PKG-INFO +64 -0
  43. splent_framework-1.2.5/src/splent_framework.egg-info/SOURCES.txt +80 -0
  44. {splent_framework-0.0.3 → splent_framework-1.2.5}/src/splent_framework.egg-info/requires.txt +4 -0
  45. splent_framework-1.2.5/tests/test_base_blueprint_security.py +149 -0
  46. splent_framework-1.2.5/tests/test_base_repository.py +168 -0
  47. splent_framework-1.2.5/tests/test_config_manager.py +76 -0
  48. splent_framework-1.2.5/tests/test_config_manager_return.py +26 -0
  49. splent_framework-1.2.5/tests/test_context_manager.py +88 -0
  50. splent_framework-1.2.5/tests/test_default_config.py +140 -0
  51. splent_framework-1.2.5/tests/test_error_handler_manager.py +109 -0
  52. splent_framework-1.2.5/tests/test_feature_manager_parse.py +82 -0
  53. splent_framework-1.2.5/tests/test_feature_order.py +233 -0
  54. splent_framework-1.2.5/tests/test_feature_utils.py +73 -0
  55. splent_framework-1.2.5/tests/test_generic_resource.py +172 -0
  56. splent_framework-1.2.5/tests/test_migration_manager.py +204 -0
  57. splent_framework-1.2.5/tests/test_namespace_manager.py +91 -0
  58. splent_framework-1.2.5/tests/test_pyproject_reader.py +176 -0
  59. splent_framework-1.2.5/tests/test_pyproject_reader_env.py +62 -0
  60. splent_framework-0.0.3/PKG-INFO +0 -26
  61. splent_framework-0.0.3/README.md +0 -1
  62. splent_framework-0.0.3/src/splent_framework/core/blueprints/base_blueprint.py +0 -72
  63. splent_framework-0.0.3/src/splent_framework/core/managers/config_manager.py +0 -65
  64. splent_framework-0.0.3/src/splent_framework/core/managers/error_handler_manager.py +0 -27
  65. splent_framework-0.0.3/src/splent_framework/core/managers/feature_manager.py +0 -72
  66. splent_framework-0.0.3/src/splent_framework/core/managers/logging_manager.py +0 -33
  67. splent_framework-0.0.3/src/splent_framework/core/managers/task_queue_manager.py +0 -52
  68. splent_framework-0.0.3/src/splent_framework/core/repositories/BaseRepository.py +0 -63
  69. splent_framework-0.0.3/src/splent_framework/core/resources/generic_resource.py +0 -81
  70. splent_framework-0.0.3/src/splent_framework/core/seeders/BaseSeeder.py +0 -60
  71. splent_framework-0.0.3/src/splent_framework/core/services/BaseService.py +0 -42
  72. splent_framework-0.0.3/src/splent_framework.egg-info/PKG-INFO +0 -26
  73. splent_framework-0.0.3/src/splent_framework.egg-info/SOURCES.txt +0 -43
  74. {splent_framework-0.0.3 → splent_framework-1.2.5}/LICENSE +0 -0
  75. {splent_framework-0.0.3 → splent_framework-1.2.5}/setup.cfg +0 -0
  76. {splent_framework-0.0.3 → splent_framework-1.2.5}/src/splent_framework/_init__.py +0 -0
  77. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework/blueprints}/__init__.py +0 -0
  78. {splent_framework-0.0.3/src/splent_framework/core/blueprints → splent_framework-1.2.5/src/splent_framework/bootstraps}/__init__.py +0 -0
  79. {splent_framework-0.0.3/src/splent_framework/core/bootstraps → splent_framework-1.2.5/src/splent_framework/configuration}/__init__.py +0 -0
  80. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/configuration/configuration.py +0 -0
  81. {splent_framework-0.0.3/src/splent_framework/core/configuration → splent_framework-1.2.5/src/splent_framework/context}/__init__.py +0 -0
  82. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/decorators/__init__.py +0 -0
  83. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/environment/__init__.py +0 -0
  84. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/environment/host.py +0 -0
  85. {splent_framework-0.0.3/src/splent_framework/core/helpers → splent_framework-1.2.5/src/splent_framework/fixtures}/__init__.py +0 -0
  86. {splent_framework-0.0.3/src/splent_framework/core/locust → splent_framework-1.2.5/src/splent_framework/helpers}/__init__.py +0 -0
  87. {splent_framework-0.0.3/src/splent_framework/core/managers → splent_framework-1.2.5/src/splent_framework/hooks}/__init__.py +0 -0
  88. {splent_framework-0.0.3/src/splent_framework/core/repositories → splent_framework-1.2.5/src/splent_framework/locust}/__init__.py +0 -0
  89. {splent_framework-0.0.3/src/splent_framework/core/resources → splent_framework-1.2.5/src/splent_framework/managers}/__init__.py +0 -0
  90. {splent_framework-0.0.3/src/splent_framework/core/seeders → splent_framework-1.2.5/src/splent_framework/migrations}/__init__.py +0 -0
  91. {splent_framework-0.0.3/src/splent_framework/core/selenium → splent_framework-1.2.5/src/splent_framework/namespaces}/__init__.py +0 -0
  92. {splent_framework-0.0.3/src/splent_framework/core/serialisers → splent_framework-1.2.5/src/splent_framework/repositories}/__init__.py +0 -0
  93. {splent_framework-0.0.3/src/splent_framework/core/services → splent_framework-1.2.5/src/splent_framework/resources}/__init__.py +0 -0
  94. {splent_framework-0.0.3/src/splent_framework/core → splent_framework-1.2.5/src/splent_framework}/selenium/common.py +0 -0
  95. {splent_framework-0.0.3 → splent_framework-1.2.5}/src/splent_framework.egg-info/dependency_links.txt +0 -0
  96. {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 = "0.0.3"
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.12"
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
- print(f"Working directory: {working_dir}")
15
+ logger.debug("Working directory: %s", working_dir)
13
16
 
14
17
  module_dir = os.path.join(working_dir, "app", "modules")
15
- print(f"Module directory: {module_dir}")
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
- print(f"Found locustfiles: {locustfile_paths}")
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
- print(f"Loading locustfile: {path}")
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
- print(f"Loaded user class: {unique_name}")
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
- print(f"Total user classes loaded: {len(found_user_classes)}")
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,3 @@
1
+ from flask_sqlalchemy import SQLAlchemy
2
+
3
+ db = SQLAlchemy()
@@ -4,9 +4,7 @@ from flask import abort
4
4
 
5
5
 
6
6
  def pass_or_abort(condition):
7
-
8
7
  def decorator(f):
9
-
10
8
  @wraps(f)
11
9
  def decorated_function(*args, **kwargs):
12
10
  if not condition(**kwargs):
@@ -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()
@@ -6,5 +6,6 @@ def login(test_client, email, password):
6
6
  )
7
7
  return response
8
8
 
9
+
9
10
  def logout(test_client):
10
11
  return test_client.get("/logout", follow_redirects=True)
@@ -1,7 +1,6 @@
1
1
  import pytest
2
- from splent_cli.utils.dynamic_imports import get_db
2
+ from splent_framework.db import db
3
3
 
4
- db = get_db()
5
4
 
6
5
  @pytest.fixture(scope="function")
7
6
  def clean_database():
@@ -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
- return token_tag["value"]
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