zecmf 0.1.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.
Files changed (39) hide show
  1. zecmf-0.1.0/MANIFEST.in +1 -0
  2. zecmf-0.1.0/PKG-INFO +43 -0
  3. zecmf-0.1.0/README.md +18 -0
  4. zecmf-0.1.0/pyproject.toml +161 -0
  5. zecmf-0.1.0/setup.cfg +4 -0
  6. zecmf-0.1.0/src/zecmf/__init__.py +7 -0
  7. zecmf-0.1.0/src/zecmf/api/__init__.py +49 -0
  8. zecmf-0.1.0/src/zecmf/app.py +79 -0
  9. zecmf-0.1.0/src/zecmf/auth/__init__.py +19 -0
  10. zecmf-0.1.0/src/zecmf/auth/decorators.py +79 -0
  11. zecmf-0.1.0/src/zecmf/auth/jwt.py +86 -0
  12. zecmf-0.1.0/src/zecmf/cli/__init__.py +5 -0
  13. zecmf-0.1.0/src/zecmf/cli/commands.py +178 -0
  14. zecmf-0.1.0/src/zecmf/config/__init__.py +60 -0
  15. zecmf-0.1.0/src/zecmf/config/base.py +184 -0
  16. zecmf-0.1.0/src/zecmf/constants.py +39 -0
  17. zecmf-0.1.0/src/zecmf/extensions/__init__.py +1 -0
  18. zecmf-0.1.0/src/zecmf/extensions/database.py +22 -0
  19. zecmf-0.1.0/src/zecmf/extensions/queue.py +37 -0
  20. zecmf-0.1.0/src/zecmf/migrations/README.template +3 -0
  21. zecmf-0.1.0/src/zecmf/migrations/__init__.py +74 -0
  22. zecmf-0.1.0/src/zecmf/migrations/alembic.ini.template +74 -0
  23. zecmf-0.1.0/src/zecmf/migrations/env.py.template +97 -0
  24. zecmf-0.1.0/src/zecmf/migrations/script.py.mako +24 -0
  25. zecmf-0.1.0/src/zecmf/testing/__init__.py +3 -0
  26. zecmf-0.1.0/src/zecmf/testing/config.py +9 -0
  27. zecmf-0.1.0/src/zecmf/testing/constants.py +16 -0
  28. zecmf-0.1.0/src/zecmf.egg-info/PKG-INFO +43 -0
  29. zecmf-0.1.0/src/zecmf.egg-info/SOURCES.txt +37 -0
  30. zecmf-0.1.0/src/zecmf.egg-info/dependency_links.txt +1 -0
  31. zecmf-0.1.0/src/zecmf.egg-info/requires.txt +17 -0
  32. zecmf-0.1.0/src/zecmf.egg-info/top_level.txt +1 -0
  33. zecmf-0.1.0/tests/test_api.py +33 -0
  34. zecmf-0.1.0/tests/test_app.py +25 -0
  35. zecmf-0.1.0/tests/test_cli.py +71 -0
  36. zecmf-0.1.0/tests/test_decorators.py +166 -0
  37. zecmf-0.1.0/tests/test_hs256_auth.py +238 -0
  38. zecmf-0.1.0/tests/test_jwt.py +94 -0
  39. zecmf-0.1.0/tests/test_migrations.py +53 -0
@@ -0,0 +1 @@
1
+ recursive-include src/zecmf/migrations *.template *.mako
zecmf-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: zecmf
3
+ Version: 0.1.0
4
+ Summary: A framework for building microservices in Python
5
+ Author-email: Hendrik Buchwald <hb@zecure.org>
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: flask==3.1.1
10
+ Requires-Dist: flask-restx==1.3.0
11
+ Requires-Dist: jsonschema==4.17.3
12
+ Requires-Dist: flask-sqlalchemy==3.1.1
13
+ Requires-Dist: flask-migrate==4.1.0
14
+ Requires-Dist: flask-jwt-extended==4.7.1
15
+ Requires-Dist: werkzeug==3.1.3
16
+ Requires-Dist: sqlalchemy==2.0.41
17
+ Requires-Dist: click==8.2.0
18
+ Requires-Dist: celery==5.5.2
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest==8.3.5; extra == "dev"
21
+ Requires-Dist: pytest-cov==6.1.1; extra == "dev"
22
+ Requires-Dist: ruff==0.11.10; extra == "dev"
23
+ Requires-Dist: mypy==1.15.0; extra == "dev"
24
+ Requires-Dist: types-Flask-Migrate==4.1.0.20250112; extra == "dev"
25
+
26
+ # Zecure Microservices Framework (ZecMF)
27
+
28
+ A lightweight framework for building microservices in Python with Flask.
29
+
30
+ ## Features
31
+
32
+ - **Application Factory**: Streamlined Flask application initialization
33
+ - **JWT Authentication**: Built-in JWT authentication with both RS256 and HS256 support
34
+ - **API Setup**: Simplified REST API initialization with Flask-RESTX
35
+ - **Database**: SQLAlchemy and Alembic integration
36
+ - **CLI Commands**: Common CLI commands for microservice management
37
+ - **Configuration**: Hierarchical configuration system with framework defaults and app-specific overrides
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install zecmf
43
+ ```
zecmf-0.1.0/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # Zecure Microservices Framework (ZecMF)
2
+
3
+ A lightweight framework for building microservices in Python with Flask.
4
+
5
+ ## Features
6
+
7
+ - **Application Factory**: Streamlined Flask application initialization
8
+ - **JWT Authentication**: Built-in JWT authentication with both RS256 and HS256 support
9
+ - **API Setup**: Simplified REST API initialization with Flask-RESTX
10
+ - **Database**: SQLAlchemy and Alembic integration
11
+ - **CLI Commands**: Common CLI commands for microservice management
12
+ - **Configuration**: Hierarchical configuration system with framework defaults and app-specific overrides
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install zecmf
18
+ ```
@@ -0,0 +1,161 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80.8", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zecmf"
7
+ version = "0.1.0"
8
+ description = "A framework for building microservices in Python"
9
+ authors = [
10
+ {name = "Hendrik Buchwald", email = "hb@zecure.org"},
11
+ ]
12
+ readme = "README.md"
13
+ requires-python = ">=3.12"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3.12",
16
+ ]
17
+ dependencies = [
18
+ "flask==3.1.1",
19
+ "flask-restx==1.3.0",
20
+ "jsonschema==4.17.3", # https://github.com/python-restx/flask-restx/issues/553#issuecomment-1639837981
21
+ "flask-sqlalchemy==3.1.1",
22
+ "flask-migrate==4.1.0",
23
+ "flask-jwt-extended==4.7.1",
24
+ "werkzeug==3.1.3",
25
+ "sqlalchemy==2.0.41",
26
+ "click==8.2.0",
27
+ "celery==5.5.2",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest==8.3.5",
33
+ "pytest-cov==6.1.1",
34
+ "ruff==0.11.10",
35
+ "mypy==1.15.0",
36
+ "types-Flask-Migrate==4.1.0.20250112",
37
+ ]
38
+
39
+ [tool.setuptools]
40
+ package-dir = {"" = "src"}
41
+ include-package-data = true
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.ruff]
47
+ line-length = 88
48
+ indent-width = 4
49
+
50
+ [tool.ruff.lint]
51
+ preview = true
52
+ unfixable = []
53
+ select = [
54
+ "E", # pycodestyle errors
55
+ "F", # pyflakes
56
+ "I", # isort
57
+ "UP", # pyupgrade
58
+ "C", # flake8-comprehensions
59
+ "B", # flake8-bugbear
60
+ "W", # pycodestyle warnings
61
+ "D", # pydocstyle
62
+ "N", # flake8-naming
63
+ "COM", # flake8-commas
64
+ "S", # flake8-bandit
65
+ "BLE", # flake8-blind-except
66
+ "A", # flake8-builtins
67
+ "PT", # flake8-pytest-style
68
+ "SIM", # flake8-simplify
69
+ "TD", # flake8-todos
70
+ "PD", # pandas-vet
71
+ "PL", # pylint
72
+ "NPY", # numpy
73
+ "RUF", # ruff-specific rules
74
+ "ANN", # flake8-annotations
75
+ "TCH", # flake8-type-checking
76
+ "TRY", # flake8-try-except-raise
77
+ ]
78
+ ignore = [
79
+ "COM812", # Missing trailing comma
80
+ "D203", # One blank line before class docstring
81
+ "D211", # No blank lines before class docstring
82
+ "D213", # Multi-line docstring summary should start at the second line
83
+ "S105", # Possible hardcoded password
84
+ "S106", # Possible hardcoded password
85
+ "PLR6301", # Method could be a function - common in API endpoints
86
+ "E501", # Line too long
87
+ "TRY003", # Avoid specifying long messages outside exception class
88
+ "PLR0913", # Too many arguments in function definition
89
+ "PLR0917", # Too many positional arguments
90
+ "S110", # try-except-pass detected, consider logging the exception
91
+ "BLE001", # Do not catch blind exception
92
+ "PLR0904", # Too many public methods
93
+ ]
94
+
95
+ [tool.ruff.lint.per-file-ignores]
96
+ "tests/*.py" = [
97
+ "ANN001", # Missing type annotation for function argument
98
+ "ANN101", # Missing type annotation for self in method
99
+ "ANN201", # Missing return type annotation for public function
100
+ "ANN204", # Missing return type annotation for special method
101
+ "S101", # Use of assert detected
102
+ ]
103
+
104
+ [tool.ruff.lint.isort]
105
+ known-first-party = ["zecmf"]
106
+ section-order = [
107
+ "future",
108
+ "standard-library",
109
+ "third-party",
110
+ "first-party",
111
+ "local-folder"
112
+ ]
113
+
114
+ [tool.ruff.format]
115
+ quote-style = "double"
116
+ indent-style = "space"
117
+ skip-magic-trailing-comma = false
118
+ line-ending = "auto"
119
+
120
+ [tool.mypy]
121
+ exclude = "build/"
122
+
123
+ # Module resolution settings
124
+ namespace_packages = true
125
+ implicit_reexport = true
126
+ follow_imports = "silent"
127
+
128
+ # Type checking strictness
129
+ disallow_untyped_defs = true
130
+ disallow_incomplete_defs = true
131
+ disallow_untyped_calls = false
132
+ check_untyped_defs = true
133
+ disallow_untyped_decorators = false
134
+ disallow_subclassing_any = false
135
+ disallow_any_unimported = false
136
+ disallow_any_generics = false
137
+
138
+ # Error reporting settings
139
+ no_implicit_optional = true
140
+ strict_optional = true
141
+ warn_redundant_casts = true
142
+ warn_no_return = true
143
+ warn_return_any = false # Don't warn about returning Any
144
+ warn_unused_ignores = false # Allow necessary type ignores
145
+
146
+ [[tool.mypy.overrides]]
147
+ module = "celery.*"
148
+ ignore_missing_imports = true
149
+
150
+ [[tool.mypy.overrides]]
151
+ module = "flask_restx.*"
152
+ ignore_missing_imports = true
153
+
154
+ [tool.pytest.ini_options]
155
+ testpaths = ["tests"]
156
+ python_files = "test_*.py"
157
+ python_classes = ["Test*"]
158
+ markers = [
159
+ "no_test: marks classes that should not be collected as tests"
160
+ ]
161
+ addopts = "--import-mode=importlib"
zecmf-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """ZecMF - A lightweight framework for building microservices in Python with Flask."""
2
+
3
+ from zecmf.app import create_app
4
+
5
+ __all__ = [
6
+ "create_app",
7
+ ]
@@ -0,0 +1,49 @@
1
+ """API module for Flask-RESTX integration."""
2
+
3
+ from typing import Any
4
+
5
+ from flask import Blueprint, Flask
6
+ from flask_restx import Api
7
+
8
+
9
+ def init_api(
10
+ app: Flask,
11
+ ) -> Api:
12
+ """Initialize the API on the Flask app.
13
+
14
+ Args:
15
+ app: The Flask application
16
+
17
+ Returns:
18
+ The configured API instance
19
+
20
+ """
21
+ # Get API configuration from app.config with defaults
22
+ prefix = app.config.get("API_PREFIX", "/api")
23
+ title = app.config.get("API_TITLE", "API")
24
+ version = app.config.get("API_VERSION", "1.0.0")
25
+ description = app.config.get("API_DESCRIPTION", "...")
26
+
27
+ # Create API blueprint with prefix
28
+ blueprint = Blueprint("api", __name__, url_prefix=prefix)
29
+
30
+ # Configure API with provided settings
31
+ api_config: dict[str, Any] = {
32
+ "doc": False,
33
+ "authorizations": {
34
+ "Bearer": {"type": "apiKey", "in": "header", "name": "Authorization"}
35
+ },
36
+ "security": "Bearer",
37
+ "title": title,
38
+ "version": version,
39
+ "description": description,
40
+ }
41
+
42
+ # Create and register API
43
+ api = Api(blueprint, **api_config)
44
+ app.register_blueprint(blueprint)
45
+
46
+ return api
47
+
48
+
49
+ __all__ = ["init_api"]
@@ -0,0 +1,79 @@
1
+ """Application factory for creating Flask microservice applications with standardized configuration."""
2
+
3
+ import logging
4
+ import os
5
+
6
+ from flask import Flask
7
+ from flask_restx import Namespace
8
+
9
+ from zecmf.api import init_api
10
+ from zecmf.auth import init_jwt
11
+ from zecmf.cli import register_commands
12
+ from zecmf.config import BaseConfig, get_config
13
+ from zecmf.constants import Config as ConfigConstants
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def create_app(
19
+ config_name: str,
20
+ api_namespaces: list[tuple[Namespace, str]],
21
+ app_config_module: str = ConfigConstants.APP_CONFIG_MODULE,
22
+ ) -> Flask:
23
+ """Create and configure a Flask microservice application.
24
+
25
+ Args:
26
+ config_name: The name of the configuration to use (development, production, etc).
27
+ api_namespaces: List of namespaces to register with the API.
28
+ app_config_module: The module path for the app-specific configuration.
29
+
30
+ Returns:
31
+ A configured Flask application.
32
+
33
+ """
34
+ logger.debug(f"Creating app with config_name: {config_name}")
35
+
36
+ if not config_name:
37
+ logger.warning(
38
+ "No config_name provided. Trying to load from environment variable."
39
+ )
40
+ config_name = os.getenv("FLASK_ENV", "development")
41
+
42
+ app_config = _resolve_and_validate_config(config_name, app_config_module)
43
+ app = _initialize_flask_app(app_config)
44
+ _setup_auth(app)
45
+ _setup_api(app, api_namespaces)
46
+ register_commands(app)
47
+ return app
48
+
49
+
50
+ def _resolve_and_validate_config(
51
+ config_name: str, app_config_module: str
52
+ ) -> BaseConfig:
53
+ """Resolve and instantiate the configuration class."""
54
+ config_class = get_config(config_name, app_config_module)
55
+ logger.debug(f"Resolved config class: {config_class.__name__}")
56
+ config_instance = config_class() # Validation on instantiation
57
+ return config_instance
58
+
59
+
60
+ def _initialize_flask_app(app_config: BaseConfig) -> Flask:
61
+ """Create and configure the Flask app instance."""
62
+ app = Flask("app")
63
+ app.config.from_object(app_config)
64
+ return app
65
+
66
+
67
+ def _setup_auth(app: Flask) -> None:
68
+ """Initialize JWT authentication."""
69
+ init_jwt(app)
70
+
71
+
72
+ def _setup_api(
73
+ app: Flask,
74
+ api_namespaces: list[tuple[Namespace, str]],
75
+ ) -> None:
76
+ """Configure API and register namespaces."""
77
+ api = init_api(app)
78
+ for namespace, path in api_namespaces:
79
+ api.add_namespace(namespace, path=path)
@@ -0,0 +1,19 @@
1
+ """Authentication module for micro-framework."""
2
+
3
+ from zecmf.auth.decorators import (
4
+ public_endpoint,
5
+ require_admin_role,
6
+ require_agent_role,
7
+ require_role,
8
+ require_user_role,
9
+ )
10
+ from zecmf.auth.jwt import init_jwt
11
+
12
+ __all__ = [
13
+ "init_jwt",
14
+ "public_endpoint",
15
+ "require_admin_role",
16
+ "require_agent_role",
17
+ "require_role",
18
+ "require_user_role",
19
+ ]
@@ -0,0 +1,79 @@
1
+ """Authentication decorators for role-based access control."""
2
+
3
+ from collections.abc import Callable
4
+ from functools import wraps
5
+ from typing import Any, TypeVar, cast
6
+
7
+ from flask import g
8
+ from werkzeug.exceptions import Forbidden
9
+
10
+ # Type variable for generic functions
11
+ F = TypeVar("F", bound=Callable[..., Any])
12
+
13
+
14
+ class DecoratorFactory:
15
+ """Factory to create decorators only when needed in a request context."""
16
+
17
+ @staticmethod
18
+ def require_role(role: str) -> Callable[[F], F]:
19
+ """Create a decorator to require a specific role."""
20
+
21
+ def decorator(f: F) -> F:
22
+ @wraps(f)
23
+ def wrapper(*args: object, **kwargs: object) -> object:
24
+ # Check if user has the required role
25
+ if not hasattr(g, "user_roles") or role not in g.user_roles:
26
+ raise Forbidden(f"Requires role: {role}")
27
+
28
+ return f(*args, **kwargs)
29
+
30
+ return cast("F", wrapper)
31
+
32
+ return decorator
33
+
34
+ @staticmethod
35
+ def public_endpoint() -> Callable[[F], F]:
36
+ """Create a decorator to mark an endpoint as public.
37
+
38
+ This decorator can be applied to:
39
+ - Regular Flask routes (function-based views)
40
+ - Specific HTTP methods in Flask-RESTX Resources (apply to method)
41
+
42
+ Note: For Flask-RESTX Resource classes, do not apply this decorator directly to
43
+ the class. Instead, apply it to individual methods.
44
+ """
45
+
46
+ def decorator(f: F) -> F:
47
+ # Mark the original function as public
48
+ f.is_public = True # type: ignore
49
+
50
+ @wraps(f)
51
+ def wrapper(*args: object, **kwargs: object) -> object:
52
+ return f(*args, **kwargs)
53
+
54
+ # Mark the wrapper function as public
55
+ wrapper.is_public = True # type: ignore
56
+
57
+ return cast("F", wrapper)
58
+
59
+ return decorator
60
+
61
+
62
+ # Create decorator factory instance
63
+ decorator_factory = DecoratorFactory()
64
+
65
+
66
+ def require_role(role: str) -> Callable[[F], F]:
67
+ """Require a specific role for an endpoint."""
68
+ return decorator_factory.require_role(role)
69
+
70
+
71
+ def public_endpoint() -> Callable[[F], F]:
72
+ """Mark an endpoint as public, exempt from authentication requirements."""
73
+ return decorator_factory.public_endpoint()
74
+
75
+
76
+ # Pre-defined role decorators for convenience
77
+ require_admin_role = require_role("admin")
78
+ require_user_role = require_role("user")
79
+ require_agent_role = require_role("agent")
@@ -0,0 +1,86 @@
1
+ """JWT authentication module for micro-framework."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any, TypeVar
5
+
6
+ from flask import Flask, Response, current_app, g, request
7
+ from flask_jwt_extended import (
8
+ JWTManager,
9
+ get_jwt,
10
+ get_jwt_identity,
11
+ verify_jwt_in_request,
12
+ )
13
+ from werkzeug.exceptions import Unauthorized
14
+
15
+ # Type variable for generic functions
16
+ F = TypeVar("F", bound=Callable[..., Any])
17
+
18
+
19
+ jwt = JWTManager()
20
+
21
+
22
+ def _check_public_endpoint(endpoint_func: Callable[..., Any]) -> bool:
23
+ """Check if an endpoint is marked as public."""
24
+ # Check if it's a class-based view (like Flask-RESTX resources)
25
+ if hasattr(endpoint_func, "view_class"):
26
+ view_class = endpoint_func.view_class
27
+
28
+ # Check class for the is_public attribute
29
+ if hasattr(view_class, "is_public") and view_class.is_public:
30
+ return True
31
+
32
+ # Check the specific method for the is_public attribute
33
+ method = request.method.lower()
34
+ if hasattr(view_class, method):
35
+ handler = getattr(view_class, method)
36
+ if hasattr(handler, "is_public") and handler.is_public:
37
+ return True
38
+
39
+ # For regular function-based views
40
+ elif hasattr(endpoint_func, "is_public") and endpoint_func.is_public:
41
+ return True
42
+
43
+ return False
44
+
45
+
46
+ def _authenticate_request() -> None:
47
+ """Authenticate the current request using JWT."""
48
+ try:
49
+ verify_jwt_in_request()
50
+
51
+ # Get JWT claims
52
+ claims = get_jwt()
53
+ user_roles = claims.get("roles", [])
54
+
55
+ # Store user info in flask g object for use in the request
56
+ g.user_id = get_jwt_identity()
57
+ g.user_roles = user_roles
58
+ except Exception as err:
59
+ raise Unauthorized("Authentication required") from err
60
+
61
+
62
+ def init_jwt(app: Flask) -> None:
63
+ """Initialize JWT manager with the Flask app."""
64
+ jwt.init_app(app)
65
+
66
+ # Register before_request handler to enforce authentication by default
67
+ @app.before_request
68
+ def enforce_authentication() -> Response | None:
69
+ """Enforce authentication for each request unless marked as public."""
70
+ # Skip OPTIONS requests for CORS support
71
+ if request.method == "OPTIONS":
72
+ return None
73
+
74
+ # Allow access to swagger.json only in debug mode
75
+ if app.debug and request.path.endswith("/swagger.json"):
76
+ return None
77
+
78
+ # Check if the endpoint is public
79
+ if request.endpoint is not None:
80
+ endpoint_func = current_app.view_functions.get(request.endpoint)
81
+ if endpoint_func is not None and _check_public_endpoint(endpoint_func):
82
+ return None
83
+
84
+ # If we're here, authentication is required
85
+ _authenticate_request()
86
+ return None
@@ -0,0 +1,5 @@
1
+ """Command-line interface module for Flask applications."""
2
+
3
+ from zecmf.cli.commands import register_commands
4
+
5
+ __all__ = ["register_commands"]