Skip to content

Commit 364c1dc

Browse files
committed
Update : Version Update and Prepared For Release -> v1.2.0
1 parent 2c93ec2 commit 364c1dc

23 files changed

+1132
-323
lines changed

jsweb/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from jsweb.validators import *
99
from jsweb.blueprints import Blueprint
1010

11-
# Make url_for easily accessible
1211
from .response import url_for
1312

14-
__VERSION__ = "1.1.0"
13+
__VERSION__ = "1.2.0"

jsweb/app.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,53 +13,76 @@ class JsWebApp:
1313
The main application class for the JsWeb framework.
1414
"""
1515
def __init__(self, config):
16+
"""
17+
Initialize the JsWebApp instance.
18+
19+
:param config: Configuration object containing settings like SECRET_KEY, TEMPLATE_FOLDER, etc.
20+
"""
1621
self.router = Router()
1722
self.template_filters = {}
1823
self.config = config
1924
self.blueprints_with_static_files = []
20-
self._init_from_config() # Initial setup
25+
self._init_from_config()
2126

2227
def _init_from_config(self):
2328
"""Initializes components that depend on the config."""
2429
template_paths = []
2530

26-
# Add the user's template folder
2731
if hasattr(self.config, "TEMPLATE_FOLDER") and hasattr(self.config, "BASE_DIR"):
2832
user_template_path = os.path.join(self.config.BASE_DIR, self.config.TEMPLATE_FOLDER)
2933
if os.path.isdir(user_template_path):
3034
template_paths.append(user_template_path)
3135

32-
# Add the library's main template folder
3336
lib_template_path = os.path.join(os.path.dirname(__file__), "templates")
3437
if os.path.isdir(lib_template_path):
3538
template_paths.append(lib_template_path)
3639

37-
# The admin templates are now self-contained in the admin package,
38-
# so we no longer add them to the main app's template paths.
39-
4040
if template_paths:
4141
configure_template_env(template_paths)
4242

4343
if hasattr(self.config, "SECRET_KEY"):
4444
init_auth(self.config.SECRET_KEY, self._get_actual_user_loader())
4545

4646
def _get_actual_user_loader(self):
47+
"""
48+
Retrieves the user loader callback.
49+
50+
:return: The user loader function.
51+
"""
4752
if hasattr(self, '_user_loader_callback') and self._user_loader_callback:
4853
return self._user_loader_callback
4954
return self.user_loader
5055

5156
def user_loader(self, user_id: int):
57+
"""
58+
Default user loader that attempts to load a user from a 'models' module.
59+
60+
:param user_id: The ID of the user to load.
61+
:return: The User object or None.
62+
"""
5263
try:
5364
from models import User
5465
return User.query.get(user_id)
5566
except (ImportError, AttributeError):
5667
return None
5768

5869
def route(self, path, methods=None, endpoint=None):
70+
"""
71+
Decorator to register a new route.
72+
73+
:param path: The URL path.
74+
:param methods: List of HTTP methods allowed.
75+
:param endpoint: Optional endpoint name.
76+
:return: The decorator function.
77+
"""
5978
return self.router.route(path, methods, endpoint)
6079

6180
def register_blueprint(self, blueprint: Blueprint):
62-
"""Registers a blueprint with the application."""
81+
"""
82+
Registers a blueprint with the application.
83+
84+
:param blueprint: The Blueprint instance to register.
85+
"""
6386
for path, handler, methods, endpoint in blueprint.routes:
6487
full_path = path
6588
if blueprint.url_prefix:
@@ -72,12 +95,25 @@ def register_blueprint(self, blueprint: Blueprint):
7295
self.blueprints_with_static_files.append(blueprint)
7396

7497
def filter(self, name):
98+
"""
99+
Decorator to register a custom template filter.
100+
101+
:param name: The name of the filter to use in templates.
102+
:return: The decorator function.
103+
"""
75104
def decorator(func):
76105
self.template_filters[name] = func
77106
return func
78107
return decorator
79108

80109
async def _asgi_app_handler(self, scope, receive, send):
110+
"""
111+
Internal ASGI handler for processing requests.
112+
113+
:param scope: The ASGI scope.
114+
:param receive: The ASGI receive channel.
115+
:param send: The ASGI send channel.
116+
"""
81117
req = scope['jsweb.request']
82118
try:
83119
handler, params = self.router.resolve(req.path, req.method)
@@ -95,7 +131,6 @@ async def _asgi_app_handler(self, scope, receive, send):
95131
return
96132

97133
if handler:
98-
# Support both sync and async handlers
99134
if asyncio.iscoroutinefunction(handler):
100135
response = await handler(req, **params)
101136
else:
@@ -107,17 +142,21 @@ async def _asgi_app_handler(self, scope, receive, send):
107142
if not isinstance(response, Response):
108143
raise TypeError(f"View function did not return a Response object (got {type(response).__name__})")
109144

110-
111-
112145
if hasattr(req, 'new_csrf_token_generated') and req.new_csrf_token_generated:
113146
response.set_cookie("csrf_token", req.csrf_token, httponly=False, samesite='Lax')
114147

115148
await response(scope, receive, send)
116149

117150

118151
async def __call__(self, scope, receive, send):
152+
"""
153+
The ASGI application entry point.
154+
155+
:param scope: The ASGI scope.
156+
:param receive: The ASGI receive channel.
157+
:param send: The ASGI send channel.
158+
"""
119159
if scope["type"] != "http":
120-
# For now, we only support http
121160
return
122161

123162
req = Request(scope, receive, self)
@@ -136,12 +175,9 @@ async def __call__(self, scope, receive, send):
136175
static_url = getattr(self.config, "STATIC_URL", "/static")
137176
static_dir = getattr(self.config, "STATIC_DIR", "static")
138177

139-
# The middleware needs to be ASGI compatible.
140-
# This will require rewriting the middleware classes.
141-
# For now, I will assume they are ASGI compatible.
142178
handler = self._asgi_app_handler
143179
handler = DBSessionMiddleware(handler)
144180
handler = StaticFilesMiddleware(handler, static_url, static_dir, blueprint_statics=self.blueprints_with_static_files)
145181
handler = CSRFMiddleware(handler)
146182

147-
await handler(scope, receive, send)
183+
await handler(scope, receive, send)

jsweb/auth.py

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,121 @@
1-
from functools import wraps
21
import asyncio
2+
from functools import wraps
3+
34
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadTimeSignature
4-
from .response import redirect, url_for, Forbidden
55

6-
# This will be initialized by the JsWebApp instance
6+
from .response import redirect, url_for
7+
78
_serializer = None
89
_user_loader = None
910

10-
def init_auth(secret_key, user_loader_func):
11-
"""Initializes the authentication system."""
11+
def init_auth(secret_key: str, user_loader_func: callable):
12+
"""
13+
Initializes the authentication system with a secret key and a user loader.
14+
15+
This function must be called once at application startup to set up the
16+
components needed for session management and user retrieval.
17+
18+
Args:
19+
secret_key: The secret key used to sign and verify session tokens.
20+
user_loader_func: A callable that takes a user ID and returns the
21+
corresponding user object, or None if not found.
22+
"""
1223
global _serializer, _user_loader
1324
_serializer = URLSafeTimedSerializer(secret_key)
1425
_user_loader = user_loader_func
1526

1627
def login_user(response, user):
17-
"""Logs a user in by creating a secure session cookie."""
28+
"""
29+
Logs a user in by creating a secure, timestamped session cookie.
30+
31+
This function serializes the user's ID and sets it in an HTTPOnly cookie
32+
on the provided response object.
33+
34+
Args:
35+
response: The Response object to which the session cookie will be attached.
36+
user: The user object to log in. Must have an 'id' attribute.
37+
"""
1838
session_token = _serializer.dumps(user.id)
1939
response.set_cookie("session", session_token, httponly=True)
2040

2141
def logout_user(response):
22-
"""Logs a user out by deleting the session cookie."""
42+
"""
43+
Logs a user out by deleting the session cookie.
44+
45+
Args:
46+
response: The Response object from which the session cookie will be removed.
47+
"""
2348
response.delete_cookie("session")
2449

2550
def get_current_user(request):
26-
"""Gets the currently logged-in user from the session cookie."""
51+
"""
52+
Retrieves the currently logged-in user from the session cookie.
53+
54+
This function deserializes the session token from the request's cookies,
55+
validates its signature and expiration (max_age=30 days), and then uses the
56+
user loader to fetch the corresponding user object.
57+
58+
Args:
59+
request: The incoming Request object.
60+
61+
Returns:
62+
The user object if a valid session exists, otherwise None.
63+
"""
2764
session_token = request.cookies.get("session")
2865
if not session_token:
2966
return None
3067

3168
try:
32-
# The max_age check (e.g., 30 days) is handled by the serializer
33-
user_id = _serializer.loads(session_token, max_age=2592000)
69+
user_id = _serializer.loads(session_token, max_age=2592000) # 30 days
3470
return _user_loader(user_id)
3571
except (SignatureExpired, BadTimeSignature):
3672
return None
3773

38-
def login_required(handler):
74+
def login_required(handler: callable) -> callable:
3975
"""
40-
A decorator to protect routes from unauthenticated access.
41-
It supports both sync and async handlers.
76+
A decorator to protect a route from unauthenticated access.
77+
78+
If the user is not logged in (i.e., `request.user` is not set), they are
79+
redirected to the login page. This decorator correctly handles both
80+
synchronous and asynchronous view functions.
81+
82+
Args:
83+
handler: The view function to protect.
84+
85+
Returns:
86+
The decorated view function.
4287
"""
4388
@wraps(handler)
4489
async def decorated_function(request, *args, **kwargs):
4590
if not request.user:
4691
login_url = url_for(request, 'auth.login')
4792
return redirect(login_url)
4893

49-
# Await the handler if it's a coroutine function
5094
if asyncio.iscoroutinefunction(handler):
5195
return await handler(request, *args, **kwargs)
5296
else:
5397
return handler(request, *args, **kwargs)
5498
return decorated_function
5599

56-
def admin_required(handler):
100+
def admin_required(handler: callable) -> callable:
57101
"""
58-
A decorator to protect routes from non-admin access.
59-
It supports both sync and async handlers.
102+
A decorator to protect a route, allowing access only to admin users.
103+
104+
This decorator checks if `request.user` exists and has an attribute
105+
`is_admin` that evaluates to True. If the check fails, the user is
106+
redirected to the admin index page. It supports both sync and async handlers.
107+
108+
Args:
109+
handler: The view function to protect.
110+
111+
Returns:
112+
The decorated view function.
60113
"""
61114
@wraps(handler)
62115
async def decorated_function(request, *args, **kwargs):
63116
if not request.user or not getattr(request.user, 'is_admin', False):
64117
return redirect(url_for(request, 'admin.index'))
65118

66-
# Await the handler if it's a coroutine function
67119
if asyncio.iscoroutinefunction(handler):
68120
return await handler(request, *args, **kwargs)
69121
else:

jsweb/blueprints.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,75 @@
1+
from typing import List, Tuple, Callable, Optional
2+
13
class Blueprint:
24
"""
3-
A self-contained, reusable component of a JsWeb application.
4-
Blueprints have their own routes which are later registered with the main app.
5+
Represents a blueprint, a collection of routes that can be registered with an application.
6+
7+
Blueprints are used to structure a JsWeb application into smaller, reusable
8+
components. Each blueprint can have its own routes, URL prefix, and static
9+
files. This helps in organizing code and promoting modularity.
510
"""
6-
def __init__(self, name, url_prefix=None, static_folder=None, static_url_path=None):
11+
def __init__(self, name: str, url_prefix: Optional[str] = None, static_folder: Optional[str] = None, static_url_path: Optional[str] = None):
712
"""
813
Initializes a new Blueprint.
914
1015
Args:
11-
name (str): The name of the blueprint.
12-
url_prefix (str, optional): A prefix to be added to all routes in this blueprint.
13-
static_folder (str, optional): The folder for static files for this blueprint.
14-
static_url_path (str, optional): The URL path for serving static files.
16+
name: The name of the blueprint, used for endpoint namespacing.
17+
url_prefix: An optional prefix for all routes defined in this blueprint.
18+
For example, if a prefix is '/api' and a route is '/users',
19+
the final path will be '/api/users'.
20+
static_folder: The name of the folder containing static files for this
21+
blueprint, relative to the blueprint's location.
22+
static_url_path: The URL path where the blueprint's static files will be
23+
served from. Defaults to the static folder name if not provided.
1524
"""
1625
self.name = name
1726
self.url_prefix = url_prefix
18-
self.routes = []
27+
self.routes: List[Tuple[str, Callable, List[str], str]] = []
1928
self.static_folder = static_folder
2029
self.static_url_path = static_url_path
2130

22-
def add_route(self, path, handler, methods=None, endpoint=None):
31+
def add_route(self, path: str, handler: Callable, methods: Optional[List[str]] = None, endpoint: Optional[str] = None):
2332
"""
24-
Programmatically adds a route to the blueprint. This is useful for
25-
dynamically generated views, like in the admin panel.
33+
Programmatically adds a route to the blueprint.
34+
35+
This method is an alternative to the `@route` decorator and is useful for
36+
dynamically generated views, such as those in a class-based view system
37+
or an admin panel.
38+
39+
Args:
40+
path: The URL path for the route.
41+
handler: The view function that will handle requests to this path.
42+
methods: A list of allowed HTTP methods (e.g., ["GET", "POST"]).
43+
Defaults to ["GET"].
44+
endpoint: A unique name for this route's endpoint. If not provided,
45+
it defaults to the name of the handler function.
2646
"""
2747
if methods is None:
2848
methods = ["GET"]
2949

30-
# If no endpoint is provided, use the function name as the default.
3150
route_endpoint = endpoint or handler.__name__
3251
self.routes.append((path, handler, methods, route_endpoint))
3352

34-
def route(self, path, methods=None, endpoint=None):
53+
def route(self, path: str, methods: Optional[List[str]] = None, endpoint: Optional[str] = None) -> Callable:
3554
"""
3655
A decorator to register a view function for a given path within the blueprint.
56+
57+
Example:
58+
bp = Blueprint('my_app')
59+
60+
@bp.route('/index', methods=['GET'])
61+
def index(request):
62+
return "Hello, World!"
63+
64+
Args:
65+
path: The URL path for the route.
66+
methods: A list of allowed HTTP methods. Defaults to ["GET"].
67+
endpoint: A unique name for the endpoint. Defaults to the function name.
68+
69+
Returns:
70+
A decorator function that registers the view.
3771
"""
38-
def decorator(handler):
72+
def decorator(handler: Callable) -> Callable:
3973
self.add_route(path, handler, methods, endpoint)
4074
return handler
4175
return decorator

0 commit comments

Comments
 (0)