orbit-web-framework 1.0.2__py3-none-any.whl

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.
orbit/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ Orbit - Lightweight Advanced Python Web Framework
3
+ Pure Standard Library Implementation
4
+
5
+ A production-minded, educational-grade web framework built entirely
6
+ with Python's standard library. No external dependencies.
7
+ """
8
+
9
+ __version__ = "1.0.2"
10
+ __author__ = "Orbit Framework Team"
11
+
12
+ from .core.application import Orbit
13
+ from .core.response import Response
14
+ from .core.request import Request
15
+
16
+ __all__ = ['Orbit', 'Response', 'Request']
orbit/core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Core module
@@ -0,0 +1,289 @@
1
+ """
2
+ Orbit Application
3
+ Main framework class orchestrating all components
4
+ """
5
+
6
+ import logging
7
+ import signal
8
+ import sys
9
+ from typing import Callable, List, Optional, Dict, Any
10
+ from .router import Router, RouterGroup
11
+ from .middleware import MiddlewareChain, Middleware
12
+ from .request import Request
13
+ from .response import Response
14
+ from ..http.server import HTTPServer
15
+ from ..static.handler import StaticFileHandler
16
+ from ..template.engine import TemplateEngine
17
+
18
+
19
+ class Orbit:
20
+ """
21
+ Main Orbit framework application
22
+ """
23
+
24
+ def __init__(self, debug: bool = False):
25
+ """
26
+ Initialize Orbit application
27
+
28
+ Args:
29
+ debug: Enable debug mode
30
+ """
31
+ self.debug = debug
32
+ self.router = Router()
33
+ self.middleware_chain = MiddlewareChain()
34
+ self.server: Optional[HTTPServer] = None
35
+
36
+ # Static file handler (optional)
37
+ self.static_handler: Optional[StaticFileHandler] = None
38
+
39
+ # Template engine (optional)
40
+ self.template_engine: Optional[TemplateEngine] = None
41
+
42
+ # Error handlers
43
+ self.error_handlers: Dict[int, Callable] = {}
44
+
45
+ # Shutdown state
46
+ self._is_shutting_down = False
47
+
48
+ # Setup logging
49
+ self._setup_logging()
50
+
51
+ # Graceful shutdown
52
+ self._setup_signal_handlers()
53
+
54
+ def _setup_logging(self):
55
+ """Setup logging configuration"""
56
+ level = logging.DEBUG if self.debug else logging.INFO
57
+
58
+ logging.basicConfig(
59
+ level=level,
60
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
61
+ datefmt='%Y-%m-%d %H:%M:%S'
62
+ )
63
+
64
+ self.logger = logging.getLogger('orbit')
65
+
66
+ def _setup_signal_handlers(self):
67
+ """Setup signal handlers for graceful shutdown"""
68
+ def signal_handler(sig, frame):
69
+ # Prevent multiple shutdown calls
70
+ if self._is_shutting_down:
71
+ return
72
+
73
+ self._is_shutting_down = True
74
+ self.logger.info("Shutdown signal received")
75
+
76
+ if self.server:
77
+ self.server.stop()
78
+
79
+ sys.exit(0)
80
+
81
+ signal.signal(signal.SIGINT, signal_handler)
82
+ signal.signal(signal.SIGTERM, signal_handler)
83
+
84
+ def route(self, path: str, methods: List[str] = None,
85
+ handler: Callable = None, name: str = None):
86
+ """
87
+ Register a route
88
+
89
+ Args:
90
+ path: URL path pattern
91
+ methods: List of HTTP methods
92
+ handler: Handler function
93
+ name: Route name
94
+
95
+ Returns:
96
+ Decorator if handler is None, else None
97
+ """
98
+ if methods is None:
99
+ methods = ['GET']
100
+
101
+ if handler is None:
102
+ # Return decorator
103
+ def decorator(func: Callable) -> Callable:
104
+ self.router.add_route(path, methods, func, name)
105
+ return func
106
+ return decorator
107
+ else:
108
+ # Direct registration
109
+ self.router.add_route(path, methods, handler, name)
110
+
111
+ def group(self, prefix: str) -> RouterGroup:
112
+ """
113
+ Create route group with prefix
114
+
115
+ Args:
116
+ prefix: URL prefix for group
117
+
118
+ Returns:
119
+ RouterGroup instance
120
+ """
121
+ return RouterGroup(self.router, prefix)
122
+
123
+ def middleware(self, middleware: Middleware):
124
+ """
125
+ Add middleware to application
126
+
127
+ Args:
128
+ middleware: Middleware instance
129
+ """
130
+ self.middleware_chain.add(middleware)
131
+
132
+ def static(self, static_dir: str, url_prefix: str = '/static',
133
+ **kwargs):
134
+ """
135
+ Enable static file serving
136
+
137
+ Args:
138
+ static_dir: Directory containing static files
139
+ url_prefix: URL prefix for static files
140
+ **kwargs: Additional StaticFileHandler options
141
+ """
142
+ self.static_handler = StaticFileHandler(static_dir, url_prefix, **kwargs)
143
+ self.logger.info(f"Static files enabled: {static_dir} -> {url_prefix}")
144
+
145
+ def templates(self, template_dir: str):
146
+ """
147
+ Enable template engine
148
+
149
+ Args:
150
+ template_dir: Directory containing templates
151
+ """
152
+ self.template_engine = TemplateEngine(template_dir)
153
+ self.logger.info(f"Templates enabled: {template_dir}")
154
+
155
+ def render(self, template_name: str, context: Dict[str, Any] = None) -> Response:
156
+ """
157
+ Render template
158
+
159
+ Args:
160
+ template_name: Template filename
161
+ context: Template context
162
+
163
+ Returns:
164
+ Response with rendered HTML
165
+ """
166
+ if not self.template_engine:
167
+ raise RuntimeError("Template engine not configured")
168
+
169
+ html = self.template_engine.render(template_name, context)
170
+ return Response.html(html)
171
+
172
+ def error_handler(self, status_code: int):
173
+ """
174
+ Register error handler decorator
175
+
176
+ Args:
177
+ status_code: HTTP status code
178
+
179
+ Returns:
180
+ Decorator function
181
+ """
182
+ def decorator(func: Callable) -> Callable:
183
+ self.error_handlers[status_code] = func
184
+ return func
185
+ return decorator
186
+
187
+ def handle_request(self, request: Request) -> Response:
188
+ """
189
+ Main request handler
190
+
191
+ Args:
192
+ request: Request object
193
+
194
+ Returns:
195
+ Response object
196
+ """
197
+ try:
198
+ # Pre-request middleware
199
+ early_response = self.middleware_chain.process_request(request)
200
+ if early_response:
201
+ return early_response
202
+
203
+ # Check static files
204
+ if self.static_handler and self.static_handler.is_static_request(request.path):
205
+ response = self.static_handler.serve(request.path)
206
+ else:
207
+ # Route matching
208
+ match = self.router.match(request.path, request.method)
209
+
210
+ if match:
211
+ handler, path_params = match
212
+ request.path_params = path_params
213
+
214
+ # Call handler
215
+ response = handler(request)
216
+
217
+ # Ensure response is Response object
218
+ if not isinstance(response, Response):
219
+ if isinstance(response, dict):
220
+ response = Response.json(response)
221
+ elif isinstance(response, str):
222
+ response = Response.html(response)
223
+ else:
224
+ response = Response(str(response))
225
+ else:
226
+ # 404 Not Found
227
+ if 404 in self.error_handlers:
228
+ response = self.error_handlers[404](request)
229
+ else:
230
+ response = Response.error(404, "Not Found")
231
+
232
+ # Post-response middleware
233
+ response = self.middleware_chain.process_response(request, response)
234
+
235
+ return response
236
+
237
+ except Exception as e:
238
+ self.logger.error(f"Request handling error: {e}", exc_info=True)
239
+
240
+ # Error handler
241
+ if 500 in self.error_handlers:
242
+ return self.error_handlers[500](request, e)
243
+
244
+ # Default error response
245
+ if self.debug:
246
+ import traceback
247
+ error_detail = traceback.format_exc()
248
+ return Response.error(500, f"Internal Server Error: {error_detail}")
249
+ else:
250
+ return Response.error(500, "Internal Server Error")
251
+
252
+ def run(self, host: str = '0.0.0.0', port: int = 8080,
253
+ use_threading: bool = True, **kwargs):
254
+ """
255
+ Run the application server
256
+
257
+ Args:
258
+ host: Host address
259
+ port: Port number
260
+ use_threading: Use threading for connections
261
+ **kwargs: Additional server options
262
+ """
263
+ self.server = HTTPServer(
264
+ host=host,
265
+ port=port,
266
+ request_handler=self.handle_request,
267
+ use_threading=use_threading,
268
+ **kwargs
269
+ )
270
+
271
+ self.logger.info(f"Starting Orbit application in {'DEBUG' if self.debug else 'PRODUCTION'} mode")
272
+
273
+ try:
274
+ self.server.start()
275
+ except KeyboardInterrupt:
276
+ self.logger.info("Shutting down...")
277
+ finally:
278
+ if self.server:
279
+ self.server.stop()
280
+
281
+ def test_client(self):
282
+ """
283
+ Create test client for testing
284
+
285
+ Returns:
286
+ TestClient instance
287
+ """
288
+ from ..testing.client import TestClient
289
+ return TestClient(self)
@@ -0,0 +1,236 @@
1
+ """
2
+ Middleware System
3
+ Chain-based middleware for request/response processing
4
+ """
5
+
6
+ from typing import Callable, List, Optional
7
+ from .request import Request
8
+ from .response import Response
9
+
10
+
11
+ class Middleware:
12
+ """
13
+ Base middleware class
14
+ """
15
+
16
+ def process_request(self, request: Request) -> Optional[Response]:
17
+ """
18
+ Process request before handler
19
+
20
+ Args:
21
+ request: Request object
22
+
23
+ Returns:
24
+ Response to short-circuit, or None to continue
25
+ """
26
+ return None
27
+
28
+ def process_response(self, request: Request, response: Response) -> Response:
29
+ """
30
+ Process response after handler
31
+
32
+ Args:
33
+ request: Request object
34
+ response: Response object
35
+
36
+ Returns:
37
+ Modified response
38
+ """
39
+ return response
40
+
41
+
42
+ class MiddlewareChain:
43
+ """
44
+ Middleware chain manager
45
+ """
46
+
47
+ def __init__(self):
48
+ self.middlewares: List[Middleware] = []
49
+
50
+ def add(self, middleware: Middleware):
51
+ """
52
+ Add middleware to chain
53
+
54
+ Args:
55
+ middleware: Middleware instance
56
+ """
57
+ self.middlewares.append(middleware)
58
+
59
+ def process_request(self, request: Request) -> Optional[Response]:
60
+ """
61
+ Run request through middleware chain
62
+
63
+ Args:
64
+ request: Request object
65
+
66
+ Returns:
67
+ Response if short-circuited, None otherwise
68
+ """
69
+ for middleware in self.middlewares:
70
+ response = middleware.process_request(request)
71
+ if response is not None:
72
+ return response
73
+ return None
74
+
75
+ def process_response(self, request: Request, response: Response) -> Response:
76
+ """
77
+ Run response through middleware chain (in reverse)
78
+
79
+ Args:
80
+ request: Request object
81
+ response: Response object
82
+
83
+ Returns:
84
+ Processed response
85
+ """
86
+ for middleware in reversed(self.middlewares):
87
+ response = middleware.process_response(request, response)
88
+ return response
89
+
90
+
91
+ # Built-in Middlewares
92
+
93
+ class SecurityHeadersMiddleware(Middleware):
94
+ """Add security headers to responses"""
95
+
96
+ def process_response(self, request: Request, response: Response) -> Response:
97
+ """Add security headers"""
98
+ response.set_header('X-Content-Type-Options', 'nosniff')
99
+ response.set_header('X-Frame-Options', 'SAMEORIGIN')
100
+ response.set_header('X-XSS-Protection', '1; mode=block')
101
+ return response
102
+
103
+
104
+ class CORSMiddleware(Middleware):
105
+ """Handle CORS headers"""
106
+
107
+ def __init__(self, allow_origins: List[str] = None,
108
+ allow_methods: List[str] = None,
109
+ allow_headers: List[str] = None):
110
+ self.allow_origins = allow_origins or ['*']
111
+ self.allow_methods = allow_methods or ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
112
+ self.allow_headers = allow_headers or ['Content-Type', 'Authorization']
113
+
114
+ def process_request(self, request: Request) -> Optional[Response]:
115
+ """Handle preflight requests"""
116
+ if request.method == 'OPTIONS':
117
+ response = Response('', status=204)
118
+ self._add_cors_headers(response)
119
+ return response
120
+ return None
121
+
122
+ def process_response(self, request: Request, response: Response) -> Response:
123
+ """Add CORS headers to response"""
124
+ self._add_cors_headers(response)
125
+ return response
126
+
127
+ def _add_cors_headers(self, response: Response):
128
+ """Add CORS headers"""
129
+ response.set_header('Access-Control-Allow-Origin', ', '.join(self.allow_origins))
130
+ response.set_header('Access-Control-Allow-Methods', ', '.join(self.allow_methods))
131
+ response.set_header('Access-Control-Allow-Headers', ', '.join(self.allow_headers))
132
+
133
+
134
+ class RateLimitMiddleware(Middleware):
135
+ """
136
+ IP-based rate limiting
137
+ """
138
+
139
+ def __init__(self, requests_per_minute: int = 60):
140
+ self.requests_per_minute = requests_per_minute
141
+ self.request_counts: dict = {} # ip -> [(timestamp, count)]
142
+
143
+ # Security logger
144
+ import logging
145
+ self.security_logger = logging.getLogger('orbit.security')
146
+
147
+ def process_request(self, request: Request) -> Optional[Response]:
148
+ """Check rate limit"""
149
+ import time
150
+
151
+ client_ip = request.client_ip
152
+ if not client_ip:
153
+ return None
154
+
155
+ current_time = time.time()
156
+
157
+ # Clean old entries
158
+ if client_ip in self.request_counts:
159
+ self.request_counts[client_ip] = [
160
+ (ts, count) for ts, count in self.request_counts[client_ip]
161
+ if current_time - ts < 60
162
+ ]
163
+ else:
164
+ self.request_counts[client_ip] = []
165
+
166
+ # Count requests in last minute
167
+ total_requests = sum(count for ts, count in self.request_counts[client_ip])
168
+
169
+ if total_requests >= self.requests_per_minute:
170
+ # Log rate limit hit
171
+ self.security_logger.warning(
172
+ f"{client_ip} | {request.method} {request.path} | RATE_LIMIT | "
173
+ f"{total_requests}/{self.requests_per_minute} req/min"
174
+ )
175
+ return Response.error(429, 'Rate limit exceeded')
176
+
177
+ # Add current request
178
+ self.request_counts[client_ip].append((current_time, 1))
179
+
180
+ return None
181
+
182
+
183
+ class LoggingMiddleware(Middleware):
184
+ """Log requests and responses"""
185
+
186
+ def __init__(self, logger):
187
+ self.logger = logger
188
+
189
+ def process_request(self, request: Request) -> Optional[Response]:
190
+ """Log incoming request"""
191
+ self.logger.info(f"{request.method} {request.path} from {request.client_ip}")
192
+ return None
193
+
194
+ def process_response(self, request: Request, response: Response) -> Response:
195
+ """Log response status"""
196
+ self.logger.info(f"{request.method} {request.path} -> {response.status}")
197
+ return response
198
+
199
+
200
+ class CSRFProtectionMiddleware(Middleware):
201
+ """
202
+ Basic CSRF token protection
203
+ """
204
+
205
+ def __init__(self):
206
+ import hashlib
207
+ import secrets
208
+ self.tokens: dict = {} # session_id -> token
209
+ self.secret = secrets.token_hex(32)
210
+
211
+ def generate_token(self, session_id: str) -> str:
212
+ """Generate CSRF token for session"""
213
+ import hashlib
214
+ import secrets
215
+
216
+ token = secrets.token_hex(32)
217
+ self.tokens[session_id] = token
218
+ return token
219
+
220
+ def process_request(self, request: Request) -> Optional[Response]:
221
+ """Validate CSRF token for state-changing requests"""
222
+ if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
223
+ # Get session ID from cookie (simplified)
224
+ session_id = request.get_header('cookie', '')
225
+
226
+ # Get token from header or form
227
+ token = request.get_header('x-csrf-token')
228
+ if not token and request.is_form():
229
+ token = request.get_form('csrf_token')
230
+
231
+ # Validate token
232
+ if session_id in self.tokens:
233
+ if token != self.tokens[session_id]:
234
+ return Response.error(403, 'CSRF token validation failed')
235
+
236
+ return None
orbit/core/request.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ Request Object
3
+ Represents an HTTP request with parsed data
4
+ """
5
+
6
+ from typing import Dict, Optional, Any
7
+ import json
8
+
9
+
10
+ class Request:
11
+ """
12
+ HTTP Request object containing all request information
13
+ """
14
+
15
+ def __init__(self, method: str, path: str, version: str,
16
+ headers: Dict[str, str], query_params: Dict[str, list],
17
+ body: bytes, parsed_body: Optional[dict] = None,
18
+ client_address: tuple = None):
19
+ """
20
+ Initialize Request object
21
+
22
+ Args:
23
+ method: HTTP method (GET, POST, etc.)
24
+ path: Request path
25
+ version: HTTP version
26
+ headers: Request headers
27
+ query_params: Query string parameters
28
+ body: Raw request body
29
+ parsed_body: Parsed body (JSON or form data)
30
+ client_address: Client IP and port tuple
31
+ """
32
+ self.method = method
33
+ self.path = path
34
+ self.version = version
35
+ self.headers = headers
36
+ self.query_params = query_params
37
+ self.body = body
38
+ self.parsed_body = parsed_body
39
+ self.client_address = client_address
40
+
41
+ # Path parameters (filled by router)
42
+ self.path_params: Dict[str, str] = {}
43
+
44
+ # Additional context storage
45
+ self.context: Dict[str, Any] = {}
46
+
47
+ @property
48
+ def client_ip(self) -> Optional[str]:
49
+ """Get client IP address"""
50
+ if self.client_address:
51
+ return self.client_address[0]
52
+ return None
53
+
54
+ def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
55
+ """
56
+ Get header value (case-insensitive)
57
+
58
+ Args:
59
+ name: Header name
60
+ default: Default value if header not found
61
+
62
+ Returns:
63
+ Header value or default
64
+ """
65
+ return self.headers.get(name.lower(), default)
66
+
67
+ def get_query(self, name: str, default: Optional[str] = None) -> Optional[str]:
68
+ """
69
+ Get query parameter value
70
+
71
+ Args:
72
+ name: Parameter name
73
+ default: Default value if not found
74
+
75
+ Returns:
76
+ Parameter value or default
77
+ """
78
+ values = self.query_params.get(name, [])
79
+ return values[0] if values else default
80
+
81
+ def get_json(self) -> Optional[dict]:
82
+ """
83
+ Get parsed JSON body
84
+
85
+ Returns:
86
+ Parsed JSON data or None
87
+ """
88
+ if self.parsed_body and isinstance(self.parsed_body, dict):
89
+ return self.parsed_body
90
+
91
+ # Try to parse if not already parsed
92
+ if self.get_header('content-type', '').startswith('application/json'):
93
+ try:
94
+ return json.loads(self.body.decode('utf-8'))
95
+ except (json.JSONDecodeError, UnicodeDecodeError):
96
+ return None
97
+
98
+ return None
99
+
100
+ def get_form(self, name: str, default: Optional[str] = None) -> Optional[str]:
101
+ """
102
+ Get form data value
103
+
104
+ Args:
105
+ name: Form field name
106
+ default: Default value if not found
107
+
108
+ Returns:
109
+ Form field value or default
110
+ """
111
+ if self.parsed_body and isinstance(self.parsed_body, dict):
112
+ return self.parsed_body.get(name, default)
113
+ return default
114
+
115
+ def is_json(self) -> bool:
116
+ """Check if request has JSON content type"""
117
+ content_type = self.get_header('content-type', '')
118
+ return 'application/json' in content_type
119
+
120
+ def is_form(self) -> bool:
121
+ """Check if request has form content type"""
122
+ content_type = self.get_header('content-type', '')
123
+ return 'application/x-www-form-urlencoded' in content_type
124
+
125
+ def __repr__(self) -> str:
126
+ return f"<Request {self.method} {self.path}>"