opalib 0.2.0__tar.gz → 0.4.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.
opalib-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: opalib
3
+ Version: 0.4.0
4
+ Summary: A library to do multiple things
5
+ Author-email: Donovan <donovandelisle7@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/DeveloperDankyMan/opalib
8
+ Project-URL: Homepage, https://github.com/DeveloperDankyMan/opalib
9
+ Keywords: physics,mesh,http,format,util
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # opalib
25
+ A library to do multiple things
26
+
27
+ ## New modules
28
+
29
+ - `src/physics.py`: gravity, force, kinetic energy, potential energy, and projectile motion helpers.
30
+ - `src/units.py`: length unit conversions including `studs`, meters, feet, and custom units.
opalib-0.4.0/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # opalib
2
+ A library to do multiple things
3
+
4
+ ## New modules
5
+
6
+ - `src/physics.py`: gravity, force, kinetic energy, potential energy, and projectile motion helpers.
7
+ - `src/units.py`: length unit conversions including `studs`, meters, feet, and custom units.
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "opalib"
3
+ version = "0.4.0"
4
+ description = "A library to do multiple things"
5
+ authors = [
6
+ { name="Donovan", email="donovandelisle7@gmail.com" }
7
+ ]
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.8"
12
+ keywords = ["physics", "mesh", "http", "format", "util"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.8",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Operating System :: OS Independent"
23
+ ]
24
+ urls = { "Repository" = "https://github.com/DeveloperDankyMan/opalib", "Homepage" = "https://github.com/DeveloperDankyMan/opalib" }
25
+
26
+ dependencies = []
27
+
28
+ [build-system]
29
+ requires = ["setuptools", "wheel"]
30
+ build-backend = "setuptools.build_meta"
31
+
32
+ [tool.setuptools]
33
+ package-dir = {"" = "src"}
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+ include = ["src*"]
38
+
39
+ [tool.bumpver]
40
+ current_version = "0.4.0"
41
+ version_pattern = "MAJOR.MINOR.PATCH"
42
+ commit_message = "bump version {old_version} -> {new_version}"
43
+ tag_message = "v{new_version}"
44
+ tag_scope = "default"
45
+
46
+ [tool.bumpver.file_patterns]
47
+ "pyproject.toml" = [
48
+ "version = \"{version}\"",
49
+ ]
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: opalib
3
+ Version: 0.4.0
4
+ Summary: A library to do multiple things
5
+ Author-email: Donovan <donovandelisle7@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/DeveloperDankyMan/opalib
8
+ Project-URL: Homepage, https://github.com/DeveloperDankyMan/opalib
9
+ Keywords: physics,mesh,http,format,util
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # opalib
25
+ A library to do multiple things
26
+
27
+ ## New modules
28
+
29
+ - `src/physics.py`: gravity, force, kinetic energy, potential energy, and projectile motion helpers.
30
+ - `src/units.py`: length unit conversions including `studs`, meters, feet, and custom units.
@@ -1,12 +1,7 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
- src/__init__.py
5
- src/enum_extender.py
6
- src/web.py
7
4
  src/opalib.egg-info/PKG-INFO
8
5
  src/opalib.egg-info/SOURCES.txt
9
6
  src/opalib.egg-info/dependency_links.txt
10
- src/opalib.egg-info/top_level.txt
11
- src/tests/enum_test.py
12
- src/tests/web_test.py
7
+ src/opalib.egg-info/top_level.txt
opalib-0.2.0/PKG-INFO DELETED
@@ -1,12 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: opalib
3
- Version: 0.2.0
4
- Summary: A library to do multiple things
5
- Author-email: Donovan <donovandelisle7@gmail.com>
6
- Requires-Python: >=3.8
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Dynamic: license-file
10
-
11
- # opalib
12
- A library to do multiple things
opalib-0.2.0/README.md DELETED
@@ -1,2 +0,0 @@
1
- # opalib
2
- A library to do multiple things
@@ -1,25 +0,0 @@
1
- [project]
2
- name = "opalib"
3
- version = "0.2.0"
4
- description = "A library to do multiple things"
5
- authors = [
6
- { name="Donovan", email="donovandelisle7@gmail.com" }
7
- ]
8
- readme = "README.md"
9
- requires-python = ">=3.8"
10
-
11
- [build-system]
12
- requires = ["setuptools"]
13
- build-backend = "setuptools.build_meta"
14
-
15
- [tool.bumpver]
16
- current_version = "0.2.0"
17
- version_pattern = "MAJOR.MINOR.PATCH"
18
- commit_message = "bump version {old_version} -> {new_version}"
19
- tag_message = "v{new_version}"
20
- tag_scope = "default"
21
-
22
- [tool.bumpver.file_patterns]
23
- "pyproject.toml" = [
24
- "version = \"{version}\"",
25
- ]
@@ -1,15 +0,0 @@
1
- # __init__.py
2
- import os
3
- import importlib
4
-
5
- # Get all .py files in this folder (excluding this file)
6
- pkg_dir = os.path.dirname(__file__)
7
- for file in os.listdir(pkg_dir):
8
- if file.endswith(".py") and file != "__init__.py":
9
- module_name = file[:-3]
10
- module = importlib.import_module(f".{module_name}", package=__name__)
11
-
12
- # Expose everything from the module to the package level
13
- for attribute in dir(module):
14
- if not attribute.startswith("_"):
15
- globals()[attribute] = getattr(module, attribute)
@@ -1,47 +0,0 @@
1
- """
2
- enum_extender - a Python version of https://github.com/buildthomas/EnumExtender
3
- """
4
-
5
- from enum import Enum as PyEnum
6
-
7
- class EnumRegistry:
8
- def __init__(self):
9
- self._enums = {}
10
-
11
- def new(self, name, items):
12
- if not isinstance(name, str):
13
- raise TypeError("Enum name must be a string")
14
-
15
- if not isinstance(items, list) or not all(isinstance(i, str) for i in items):
16
- raise TypeError("Enum items must be a list of strings")
17
-
18
- if name in self._enums:
19
- raise ValueError(f"Enum '{name}' already exists")
20
-
21
- # Create a real Python Enum class dynamically
22
- enum_class = PyEnum(name, {item: index for index, item in enumerate(items)})
23
-
24
- self._enums[name] = enum_class
25
- return enum_class
26
-
27
- def __getattr__(self, name):
28
- if name in self._enums:
29
- return self._enums[name]
30
- raise AttributeError(f"Enum '{name}' does not exist")
31
-
32
- def find(self, name):
33
- return self._enums.get(name)
34
-
35
- def from_value(self, enum_name, value):
36
- enum_class = self._enums.get(enum_name)
37
- if enum_class is None:
38
- return None
39
-
40
- for item in enum_class:
41
- if item.value == value:
42
- return item
43
- return None
44
-
45
-
46
- # Exported instance
47
- Enums = EnumRegistry()
@@ -1,12 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: opalib
3
- Version: 0.2.0
4
- Summary: A library to do multiple things
5
- Author-email: Donovan <donovandelisle7@gmail.com>
6
- Requires-Python: >=3.8
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Dynamic: license-file
10
-
11
- # opalib
12
- A library to do multiple things
@@ -1,4 +0,0 @@
1
- __init__
2
- enum_extender
3
- tests
4
- web
@@ -1,4 +0,0 @@
1
- from ..enum_extender import Enums
2
-
3
- Enums.new("HTTPMethod", ["GET", "POST", "PUT", "DELETE"])
4
- print(Enums.HTTPMethod.GET)
@@ -1,329 +0,0 @@
1
- """
2
- Unit tests for opalib.web framework.
3
-
4
- Tests core functionality including routing, request/response handling, and middleware.
5
- """
6
-
7
- import unittest
8
- from io import BytesIO
9
- from src.web import (
10
- Application,
11
- Request,
12
- Response,
13
- Router,
14
- Middleware,
15
- JSONMiddleware,
16
- CORSMiddleware,
17
- )
18
-
19
-
20
- class TestRequest(unittest.TestCase):
21
- """Test Request class."""
22
-
23
- def test_request_creation(self):
24
- """Test basic request creation."""
25
- environ = {
26
- "REQUEST_METHOD": "GET",
27
- "PATH_INFO": "/users/123",
28
- "QUERY_STRING": "page=1&limit=10",
29
- "HTTP_USER_AGENT": "TestClient",
30
- "wsgi.input": BytesIO(b""),
31
- "CONTENT_LENGTH": "0",
32
- }
33
-
34
- request = Request(environ)
35
-
36
- self.assertEqual(request.method, "GET")
37
- self.assertEqual(request.path, "/users/123")
38
- self.assertEqual(request.query_string, "page=1&limit=10")
39
- self.assertEqual(request.headers.get("User-Agent"), "TestClient")
40
-
41
- def test_request_json_body(self):
42
- """Test JSON body parsing."""
43
- json_data = b'{"name": "John", "age": 30}'
44
- environ = {
45
- "REQUEST_METHOD": "POST",
46
- "PATH_INFO": "/users",
47
- "QUERY_STRING": "",
48
- "wsgi.input": BytesIO(json_data),
49
- "CONTENT_LENGTH": str(len(json_data)),
50
- }
51
-
52
- request = Request(environ)
53
- self.assertEqual(request.json["name"], "John")
54
- self.assertEqual(request.json["age"], 30)
55
-
56
- def test_request_query_params(self):
57
- """Test query parameter parsing."""
58
- environ = {
59
- "REQUEST_METHOD": "GET",
60
- "PATH_INFO": "/search",
61
- "QUERY_STRING": "q=python&limit=20",
62
- "wsgi.input": BytesIO(b""),
63
- "CONTENT_LENGTH": "0",
64
- }
65
-
66
- request = Request(environ)
67
- self.assertEqual(request.get_query_param("q"), "python")
68
- self.assertEqual(request.get_query_param("limit"), "20")
69
- self.assertEqual(request.get_query_param("missing", "default"), "default")
70
-
71
- def test_request_cookies(self):
72
- """Test cookie parsing."""
73
- environ = {
74
- "REQUEST_METHOD": "GET",
75
- "PATH_INFO": "/",
76
- "QUERY_STRING": "",
77
- "HTTP_COOKIE": "session_id=abc123; user=john",
78
- "wsgi.input": BytesIO(b""),
79
- "CONTENT_LENGTH": "0",
80
- }
81
-
82
- request = Request(environ)
83
- self.assertEqual(request.cookies.get("session_id"), "abc123")
84
- self.assertEqual(request.cookies.get("user"), "john")
85
-
86
-
87
- class TestResponse(unittest.TestCase):
88
- """Test Response class."""
89
-
90
- def test_response_creation(self):
91
- """Test basic response creation."""
92
- response = Response("Hello", status=200, content_type="text/plain")
93
-
94
- self.assertEqual(response.status, 200)
95
- self.assertEqual(response.content, "Hello")
96
- self.assertEqual(response.content_type, "text/plain")
97
-
98
- def test_response_json(self):
99
- """Test JSON response."""
100
- data = {"message": "success"}
101
- response = Response(data, content_type="application/json")
102
-
103
- self.assertEqual(response.content, '{"message": "success"}')
104
-
105
- def test_response_cookie(self):
106
- """Test cookie setting."""
107
- response = Response("test")
108
- response.set_cookie("session", "abc123", path="/", http_only=True)
109
-
110
- headers = response.get_headers()
111
- cookie_headers = [h for h in headers if h[0] == "Set-Cookie"]
112
-
113
- self.assertEqual(len(cookie_headers), 1)
114
- self.assertIn("session=abc123", cookie_headers[0][1])
115
- self.assertIn("HttpOnly", cookie_headers[0][1])
116
-
117
- def test_response_status_string(self):
118
- """Test status string generation."""
119
- response = Response("test", status=404)
120
- self.assertEqual(response.get_status_string(), "404 Not Found")
121
-
122
- response = Response("test", status=201)
123
- self.assertEqual(response.get_status_string(), "201 Created")
124
-
125
-
126
- class TestRouter(unittest.TestCase):
127
- """Test Router class."""
128
-
129
- def setUp(self):
130
- """Set up test fixtures."""
131
- self.router = Router()
132
-
133
- def test_simple_route(self):
134
- """Test simple route matching."""
135
-
136
- def handler(request):
137
- return "ok"
138
-
139
- self.router.add_route("/", ["GET"], handler)
140
- matched_handler, params = self.router.match_route("GET", "/")
141
-
142
- self.assertIsNotNone(matched_handler)
143
- self.assertEqual(params, {})
144
-
145
- def test_parametrized_route(self):
146
- """Test route with parameters."""
147
-
148
- def handler(request):
149
- return "ok"
150
-
151
- self.router.add_route("/users/<user_id>", ["GET"], handler)
152
- matched_handler, params = self.router.match_route("GET", "/users/123")
153
-
154
- self.assertIsNotNone(matched_handler)
155
- self.assertEqual(params["user_id"], "123")
156
-
157
- def test_multiple_parameters(self):
158
- """Test route with multiple parameters."""
159
-
160
- def handler(request):
161
- return "ok"
162
-
163
- self.router.add_route("/posts/<post_id>/comments/<comment_id>", ["GET"], handler)
164
- matched_handler, params = self.router.match_route(
165
- "GET", "/posts/456/comments/789"
166
- )
167
-
168
- self.assertIsNotNone(matched_handler)
169
- self.assertEqual(params["post_id"], "456")
170
- self.assertEqual(params["comment_id"], "789")
171
-
172
- def test_method_matching(self):
173
- """Test HTTP method matching."""
174
-
175
- def get_handler(request):
176
- return "get"
177
-
178
- def post_handler(request):
179
- return "post"
180
-
181
- self.router.add_route("/items", ["GET"], get_handler)
182
- self.router.add_route("/items", ["POST"], post_handler)
183
-
184
- get_matched, _ = self.router.match_route("GET", "/items")
185
- post_matched, _ = self.router.match_route("POST", "/items")
186
-
187
- self.assertEqual(get_matched(None), "get")
188
- self.assertEqual(post_matched(None), "post")
189
-
190
- def test_no_route_match(self):
191
- """Test when no route matches."""
192
- handler, params = self.router.match_route("GET", "/nonexistent")
193
-
194
- self.assertIsNone(handler)
195
- self.assertEqual(params, {})
196
-
197
-
198
- class TestMiddleware(unittest.TestCase):
199
- """Test Middleware functionality."""
200
-
201
- def test_json_middleware(self):
202
- """Test JSON middleware auto-conversion."""
203
- middleware = JSONMiddleware()
204
- response = Response({"message": "test"}, content_type="text/plain")
205
-
206
- processed = middleware.process_response(response)
207
-
208
- self.assertEqual(processed.content_type, "application/json")
209
-
210
- def test_cors_middleware(self):
211
- """Test CORS middleware."""
212
- middleware = CORSMiddleware(allowed_origins=["http://example.com"])
213
- response = Response("test")
214
-
215
- processed = middleware.process_response(response)
216
-
217
- self.assertIn("Access-Control-Allow-Origin", processed.headers)
218
- self.assertIn("Access-Control-Allow-Methods", processed.headers)
219
-
220
-
221
- class TestApplication(unittest.TestCase):
222
- """Test Application class."""
223
-
224
- def setUp(self):
225
- """Set up test fixtures."""
226
- self.app = Application("TestApp")
227
-
228
- def test_app_creation(self):
229
- """Test application creation."""
230
- self.assertEqual(self.app.name, "TestApp")
231
- self.assertIsNotNone(self.app.router)
232
- self.assertGreater(len(self.app.middlewares), 0)
233
-
234
- def test_route_decorator(self):
235
- """Test route decorator."""
236
-
237
- @self.app.get("/test")
238
- def test_handler(request):
239
- return {"status": "ok"}
240
-
241
- handler, _ = self.app.router.match_route("GET", "/test")
242
- self.assertIsNotNone(handler)
243
-
244
- def test_post_decorator(self):
245
- """Test POST decorator."""
246
-
247
- @self.app.post("/submit")
248
- def submit_handler(request):
249
- return {"status": "submitted"}
250
-
251
- handler, _ = self.app.router.match_route("POST", "/submit")
252
- self.assertIsNotNone(handler)
253
-
254
- def test_wsgi_app_not_found(self):
255
- """Test WSGI app 404 response."""
256
- environ = {
257
- "REQUEST_METHOD": "GET",
258
- "PATH_INFO": "/nonexistent",
259
- "QUERY_STRING": "",
260
- "wsgi.input": BytesIO(b""),
261
- "CONTENT_LENGTH": "0",
262
- }
263
-
264
- responses = []
265
-
266
- def start_response(status, headers):
267
- responses.append({"status": status, "headers": headers})
268
-
269
- body = self.app.wsgi_app(environ, start_response)
270
- status = responses[0]["status"]
271
-
272
- self.assertIn("404", status)
273
-
274
- def test_wsgi_app_with_route(self):
275
- """Test WSGI app with matching route."""
276
-
277
- @self.app.get("/hello")
278
- def hello(request):
279
- return {"message": "Hello"}
280
-
281
- environ = {
282
- "REQUEST_METHOD": "GET",
283
- "PATH_INFO": "/hello",
284
- "QUERY_STRING": "",
285
- "wsgi.input": BytesIO(b""),
286
- "CONTENT_LENGTH": "0",
287
- }
288
-
289
- responses = []
290
-
291
- def start_response(status, headers):
292
- responses.append({"status": status, "headers": headers})
293
-
294
- body = self.app.wsgi_app(environ, start_response)
295
- status = responses[0]["status"]
296
-
297
- self.assertIn("200", status)
298
-
299
- def test_wsgi_app_json_body(self):
300
- """Test WSGI app with JSON POST."""
301
-
302
- @self.app.post("/users")
303
- def create_user(request):
304
- data = request.json
305
- return {"id": 1, "name": data.get("name")}
306
-
307
- json_data = b'{"name":"John"}'
308
- environ = {
309
- "REQUEST_METHOD": "POST",
310
- "PATH_INFO": "/users",
311
- "QUERY_STRING": "",
312
- "CONTENT_TYPE": "application/json",
313
- "wsgi.input": BytesIO(json_data),
314
- "CONTENT_LENGTH": str(len(json_data)),
315
- }
316
-
317
- responses = []
318
-
319
- def start_response(status, headers):
320
- responses.append({"status": status, "headers": headers})
321
-
322
- body = self.app.wsgi_app(environ, start_response)
323
- status = responses[0]["status"]
324
-
325
- self.assertIn("200", status)
326
-
327
-
328
- if __name__ == "__main__":
329
- unittest.main()
opalib-0.2.0/src/web.py DELETED
@@ -1,555 +0,0 @@
1
- """
2
- opalib.web - A fully custom web framework for building web applications.
3
-
4
- This module provides a minimal but complete web framework similar to Flask,
5
- built from scratch without external web framework dependencies.
6
- """
7
-
8
- import json
9
- import re
10
- import mimetypes
11
- from typing import Callable, Dict, List, Any, Optional, Tuple, Union
12
- from urllib.parse import parse_qs, urlparse, unquote
13
- from datetime import datetime
14
- from pathlib import Path
15
- from wsgiref.simple_server import make_server
16
- import traceback
17
-
18
-
19
- class Request:
20
- """Represents an HTTP request."""
21
-
22
- def __init__(self, environ: Dict[str, Any]):
23
- """
24
- Initialize a Request object from WSGI environ dictionary.
25
-
26
- Args:
27
- environ: WSGI environment dictionary
28
- """
29
- self.environ = environ
30
- self.method = environ.get("REQUEST_METHOD", "GET").upper()
31
- self.path = environ.get("PATH_INFO", "/")
32
- self.query_string = environ.get("QUERY_STRING", "")
33
- self.headers = self._parse_headers()
34
- self.cookies = self._parse_cookies()
35
- self._body = None
36
- self._json = None
37
- self._form = None
38
- self.route_params: Dict[str, Any] = {}
39
-
40
- def _parse_headers(self) -> Dict[str, str]:
41
- """Parse HTTP headers from WSGI environ."""
42
- headers = {}
43
- for key, value in self.environ.items():
44
- if key.startswith("HTTP_"):
45
- header_name = key[5:].replace("_", "-").title()
46
- headers[header_name] = value
47
- return headers
48
-
49
- def _parse_cookies(self) -> Dict[str, str]:
50
- """Parse cookies from request headers."""
51
- cookies = {}
52
- cookie_header = self.headers.get("Cookie", "")
53
- if cookie_header:
54
- for cookie in cookie_header.split(";"):
55
- if "=" in cookie:
56
- name, value = cookie.split("=", 1)
57
- cookies[name.strip()] = value.strip()
58
- return cookies
59
-
60
- @property
61
- def body(self) -> str:
62
- """Get raw request body."""
63
- if self._body is None:
64
- try:
65
- content_length = int(self.environ.get("CONTENT_LENGTH", 0))
66
- except ValueError:
67
- content_length = 0
68
-
69
- if content_length > 0:
70
- self._body = self.environ["wsgi.input"].read(content_length).decode("utf-8")
71
- else:
72
- self._body = ""
73
- return self._body
74
-
75
- @property
76
- def json(self) -> Optional[Dict[str, Any]]:
77
- """Get parsed JSON body."""
78
- if self._json is None and self.body:
79
- try:
80
- self._json = json.loads(self.body)
81
- except json.JSONDecodeError:
82
- self._json = None
83
- return self._json
84
-
85
- @property
86
- def query_params(self) -> Dict[str, List[str]]:
87
- """Get query parameters."""
88
- return parse_qs(self.query_string)
89
-
90
- @property
91
- def form_data(self) -> Dict[str, List[str]]:
92
- """Get form data from POST body."""
93
- if self._form is None:
94
- self._form = parse_qs(self.body)
95
- return self._form
96
-
97
- def get_query_param(self, key: str, default: Optional[str] = None) -> Optional[str]:
98
- """Get a single query parameter."""
99
- params = self.query_params.get(key, [])
100
- return params[0] if params else default
101
-
102
- def get_form_value(self, key: str, default: Optional[str] = None) -> Optional[str]:
103
- """Get a single form field value."""
104
- form_data = self.form_data.get(key, [])
105
- return form_data[0] if form_data else default
106
-
107
-
108
- class Response:
109
- """Represents an HTTP response."""
110
-
111
- def __init__(
112
- self,
113
- content: Union[str, Dict, bytes] = "",
114
- status: int = 200,
115
- headers: Optional[Dict[str, str]] = None,
116
- content_type: str = "text/plain",
117
- ):
118
- """
119
- Initialize a Response object.
120
-
121
- Args:
122
- content: Response body content
123
- status: HTTP status code
124
- headers: Additional HTTP headers
125
- content_type: Content-Type header value
126
- """
127
- self.status = status
128
- self.headers = headers or {}
129
- self.content_type = content_type
130
- self._content = content
131
- self.cookies: Dict[str, Dict[str, str]] = {}
132
-
133
- @property
134
- def content(self) -> Union[str, bytes]:
135
- """Get response content."""
136
- if isinstance(self._content, dict):
137
- return json.dumps(self._content)
138
- return self._content
139
-
140
- @content.setter
141
- def content(self, value: Union[str, Dict, bytes]):
142
- """Set response content."""
143
- self._content = value
144
-
145
- def set_cookie(
146
- self,
147
- name: str,
148
- value: str,
149
- max_age: Optional[int] = None,
150
- path: str = "/",
151
- secure: bool = False,
152
- http_only: bool = False,
153
- ):
154
- """
155
- Set a response cookie.
156
-
157
- Args:
158
- name: Cookie name
159
- value: Cookie value
160
- max_age: Cookie lifetime in seconds
161
- path: Cookie path
162
- secure: HTTPS only flag
163
- http_only: JavaScript inaccessible flag
164
- """
165
- self.cookies[name] = {
166
- "value": value,
167
- "path": path,
168
- "max_age": max_age,
169
- "secure": secure,
170
- "http_only": http_only,
171
- }
172
-
173
- def get_headers(self) -> List[Tuple[str, str]]:
174
- """Get all headers as list of tuples for WSGI."""
175
- headers = [("Content-Type", self.content_type)]
176
- headers.extend(self.headers.items())
177
-
178
- for name, cookie_data in self.cookies.items():
179
- cookie_str = f"{name}={cookie_data['value']}"
180
- if cookie_data.get("path"):
181
- cookie_str += f"; Path={cookie_data['path']}"
182
- if cookie_data.get("max_age"):
183
- cookie_str += f"; Max-Age={cookie_data['max_age']}"
184
- if cookie_data.get("secure"):
185
- cookie_str += "; Secure"
186
- if cookie_data.get("http_only"):
187
- cookie_str += "; HttpOnly"
188
- headers.append(("Set-Cookie", cookie_str))
189
-
190
- return headers
191
-
192
- def get_status_string(self) -> str:
193
- """Get HTTP status string."""
194
- status_messages = {
195
- 200: "OK",
196
- 201: "Created",
197
- 204: "No Content",
198
- 301: "Moved Permanently",
199
- 302: "Found",
200
- 304: "Not Modified",
201
- 400: "Bad Request",
202
- 401: "Unauthorized",
203
- 403: "Forbidden",
204
- 404: "Not Found",
205
- 405: "Method Not Allowed",
206
- 500: "Internal Server Error",
207
- 502: "Bad Gateway",
208
- 503: "Service Unavailable",
209
- }
210
- message = status_messages.get(self.status, "Unknown")
211
- return f"{self.status} {message}"
212
-
213
-
214
- class Router:
215
- """Handles URL routing and dispatching."""
216
-
217
- def __init__(self):
218
- """Initialize the router."""
219
- self.routes: Dict[str, List[Tuple[str, Callable]]] = {}
220
- self.before_request: List[Callable] = []
221
- self.after_request: List[Callable] = []
222
-
223
- def add_route(
224
- self,
225
- path: str,
226
- methods: List[str],
227
- handler: Callable,
228
- ):
229
- """
230
- Add a route to the router.
231
-
232
- Args:
233
- path: URL path (can include parameters like /users/<id>)
234
- methods: List of HTTP methods
235
- handler: Handler function
236
- """
237
- for method in methods:
238
- method = method.upper()
239
- if method not in self.routes:
240
- self.routes[method] = []
241
- self.routes[method].append((path, handler))
242
-
243
- def match_route(
244
- self, method: str, path: str
245
- ) -> Tuple[Optional[Callable], Dict[str, Any]]:
246
- """
247
- Match a request to a route.
248
-
249
- Args:
250
- method: HTTP method
251
- path: URL path
252
-
253
- Returns:
254
- Tuple of (handler, route_params)
255
- """
256
- method = method.upper()
257
- routes_for_method = self.routes.get(method, [])
258
-
259
- for route_pattern, handler in routes_for_method:
260
- params, match = self._match_pattern(route_pattern, path)
261
- if match:
262
- return handler, params
263
-
264
- return None, {}
265
-
266
- @staticmethod
267
- def _match_pattern(pattern: str, path: str) -> Tuple[Dict[str, str], bool]:
268
- """
269
- Match a URL pattern against a path.
270
-
271
- Args:
272
- pattern: Route pattern (e.g., /users/<id>/posts/<post_id>)
273
- path: Request path
274
-
275
- Returns:
276
- Tuple of (params_dict, is_match)
277
- """
278
- param_regex = re.compile(r"<(\w+)>")
279
- params = {}
280
-
281
- pattern_parts = pattern.split("/")
282
- path_parts = path.split("/")
283
-
284
- if len(pattern_parts) != len(path_parts):
285
- return {}, False
286
-
287
- for pattern_part, path_part in zip(pattern_parts, path_parts):
288
- if pattern_part.startswith("<") and pattern_part.endswith(">"):
289
- param_name = pattern_part[1:-1]
290
- params[param_name] = unquote(path_part)
291
- elif pattern_part != path_part:
292
- return {}, False
293
-
294
- return params, True
295
-
296
-
297
- class Middleware:
298
- """Base class for middleware."""
299
-
300
- def process_request(self, request: Request) -> Optional[Response]:
301
- """
302
- Process request before handler.
303
-
304
- Args:
305
- request: Request object
306
-
307
- Returns:
308
- Response object to short-circuit, or None to continue
309
- """
310
- return None
311
-
312
- def process_response(self, response: Response) -> Response:
313
- """
314
- Process response after handler.
315
-
316
- Args:
317
- response: Response object
318
-
319
- Returns:
320
- Modified response object
321
- """
322
- return response
323
-
324
-
325
- class JSONMiddleware(Middleware):
326
- """Middleware for automatic JSON response conversion."""
327
-
328
- def process_response(self, response: Response) -> Response:
329
- """Convert dict responses to JSON automatically."""
330
- if isinstance(response._content, dict):
331
- response.content_type = "application/json"
332
- return response
333
-
334
-
335
- class CORSMiddleware(Middleware):
336
- """Middleware for CORS support."""
337
-
338
- def __init__(self, allowed_origins: List[str] = None):
339
- """
340
- Initialize CORS middleware.
341
-
342
- Args:
343
- allowed_origins: List of allowed origins (* for all)
344
- """
345
- self.allowed_origins = allowed_origins or ["*"]
346
-
347
- def process_response(self, response: Response) -> Response:
348
- """Add CORS headers."""
349
- origins = ", ".join(self.allowed_origins)
350
- response.headers["Access-Control-Allow-Origin"] = origins
351
- response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS"
352
- response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
353
- return response
354
-
355
-
356
- class Application:
357
- """Main web application class."""
358
-
359
- def __init__(self, name: str = "WebApp"):
360
- """
361
- Initialize the application.
362
-
363
- Args:
364
- name: Application name
365
- """
366
- self.name = name
367
- self.router = Router()
368
- self.middlewares: List[Middleware] = []
369
- self._add_default_middlewares()
370
-
371
- def _add_default_middlewares(self):
372
- """Add default middleware."""
373
- self.use(JSONMiddleware())
374
- self.use(CORSMiddleware())
375
-
376
- def use(self, middleware: Middleware):
377
- """
378
- Register middleware.
379
-
380
- Args:
381
- middleware: Middleware instance
382
- """
383
- self.middlewares.append(middleware)
384
-
385
- def route(
386
- self,
387
- path: str,
388
- methods: List[str] = None,
389
- ):
390
- """
391
- Decorator for registering routes.
392
-
393
- Args:
394
- path: URL path
395
- methods: HTTP methods (default: ['GET'])
396
-
397
- Returns:
398
- Decorator function
399
- """
400
- if methods is None:
401
- methods = ["GET"]
402
-
403
- def decorator(handler):
404
- self.router.add_route(path, methods, handler)
405
- return handler
406
-
407
- return decorator
408
-
409
- def get(self, path: str):
410
- """Decorator for GET routes."""
411
- return self.route(path, ["GET"])
412
-
413
- def post(self, path: str):
414
- """Decorator for POST routes."""
415
- return self.route(path, ["POST"])
416
-
417
- def put(self, path: str):
418
- """Decorator for PUT routes."""
419
- return self.route(path, ["PUT"])
420
-
421
- def delete(self, path: str):
422
- """Decorator for DELETE routes."""
423
- return self.route(path, ["DELETE"])
424
-
425
- def patch(self, path: str):
426
- """Decorator for PATCH routes."""
427
- return self.route(path, ["PATCH"])
428
-
429
- def static(self, url_prefix: str, folder_path: str):
430
- """
431
- Serve static files.
432
-
433
- Args:
434
- url_prefix: URL prefix (e.g., /static)
435
- folder_path: Local folder path
436
- """
437
-
438
- def serve_static(request: Request) -> Response:
439
- file_path = Path(folder_path) / request.path.replace(url_prefix, "", 1).lstrip("/")
440
- file_path = file_path.resolve()
441
-
442
- if not str(file_path).startswith(str(Path(folder_path).resolve())):
443
- return Response("Forbidden", status=403)
444
-
445
- if file_path.is_file():
446
- with open(file_path, "rb") as f:
447
- content_type, _ = mimetypes.guess_type(str(file_path))
448
- return Response(
449
- f.read(),
450
- content_type=content_type or "application/octet-stream",
451
- )
452
-
453
- return Response("Not Found", status=404)
454
-
455
- self.router.add_route(f"{url_prefix}/<path>", ["GET"], serve_static)
456
-
457
- def wsgi_app(self, environ: Dict[str, Any], start_response: Callable):
458
- """
459
- WSGI application interface.
460
-
461
- Args:
462
- environ: WSGI environment
463
- start_response: WSGI start_response callable
464
-
465
- Returns:
466
- Response body
467
- """
468
- request = Request(environ)
469
-
470
- try:
471
- # Process request middleware
472
- for middleware in self.middlewares:
473
- response = middleware.process_request(request)
474
- if response:
475
- break
476
- else:
477
- # Route the request
478
- handler, route_params = self.router.match_route(request.method, request.path)
479
-
480
- if handler:
481
- request.route_params = route_params
482
- result = handler(request)
483
-
484
- if isinstance(result, Response):
485
- response = result
486
- elif isinstance(result, dict):
487
- response = Response(result, content_type="application/json")
488
- elif isinstance(result, str):
489
- response = Response(result)
490
- else:
491
- response = Response(str(result))
492
- else:
493
- response = Response(
494
- json.dumps({"error": "Not Found"}),
495
- status=404,
496
- content_type="application/json",
497
- )
498
-
499
- # Process response middleware
500
- for middleware in self.middlewares:
501
- response = middleware.process_response(response)
502
-
503
- except Exception as e:
504
- error_response = Response(
505
- json.dumps({"error": str(e), "traceback": traceback.format_exc()}),
506
- status=500,
507
- content_type="application/json",
508
- )
509
- response = error_response
510
-
511
- # Prepare WSGI response
512
- status_string = response.get_status_string()
513
- headers = response.get_headers()
514
-
515
- start_response(status_string, headers)
516
-
517
- if isinstance(response.content, bytes):
518
- return [response.content]
519
- return [response.content.encode("utf-8")]
520
-
521
- def run(self, host: str = "127.0.0.1", port: int = 8000, debug: bool = True):
522
- """
523
- Start the development server.
524
-
525
- Args:
526
- host: Host address
527
- port: Port number
528
- debug: Debug mode flag
529
- """
530
- print(f"Starting {self.name} on {host}:{port}")
531
- if debug:
532
- print("Debug mode: ON")
533
- print("Press CTRL+C to quit")
534
-
535
- server = make_server(host, port, self.wsgi_app)
536
-
537
- try:
538
- server.serve_forever()
539
- except KeyboardInterrupt:
540
- print("\nShutting down server...")
541
- server.server_close()
542
-
543
-
544
- # Utility function for quick app creation
545
- def create_app(name: str = "WebApp") -> Application:
546
- """
547
- Create a new web application instance.
548
-
549
- Args:
550
- name: Application name
551
-
552
- Returns:
553
- Application instance
554
- """
555
- return Application(name)
File without changes
File without changes