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 +30 -0
- opalib-0.4.0/README.md +7 -0
- opalib-0.4.0/pyproject.toml +49 -0
- opalib-0.4.0/src/opalib.egg-info/PKG-INFO +30 -0
- {opalib-0.2.0 → opalib-0.4.0}/src/opalib.egg-info/SOURCES.txt +1 -6
- opalib-0.4.0/src/opalib.egg-info/top_level.txt +1 -0
- opalib-0.2.0/PKG-INFO +0 -12
- opalib-0.2.0/README.md +0 -2
- opalib-0.2.0/pyproject.toml +0 -25
- opalib-0.2.0/src/__init__.py +0 -15
- opalib-0.2.0/src/enum_extender.py +0 -47
- opalib-0.2.0/src/opalib.egg-info/PKG-INFO +0 -12
- opalib-0.2.0/src/opalib.egg-info/top_level.txt +0 -4
- opalib-0.2.0/src/tests/enum_test.py +0 -4
- opalib-0.2.0/src/tests/web_test.py +0 -329
- opalib-0.2.0/src/web.py +0 -555
- {opalib-0.2.0 → opalib-0.4.0}/LICENSE +0 -0
- {opalib-0.2.0 → opalib-0.4.0}/setup.cfg +0 -0
- {opalib-0.2.0 → opalib-0.4.0}/src/opalib.egg-info/dependency_links.txt +0 -0
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,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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
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
opalib-0.2.0/pyproject.toml
DELETED
|
@@ -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
|
-
]
|
opalib-0.2.0/src/__init__.py
DELETED
|
@@ -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,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
|
|
File without changes
|