simpserver 0.1.1.post1__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.
- simpserver/database.py +15 -0
- simpserver/exceptions.py +62 -0
- simpserver/handler.py +112 -0
- simpserver/router.py +187 -0
- simpserver-0.1.1.post1.dist-info/METADATA +19 -0
- simpserver-0.1.1.post1.dist-info/RECORD +9 -0
- simpserver-0.1.1.post1.dist-info/WHEEL +5 -0
- simpserver-0.1.1.post1.dist-info/licenses/LICENSE +13 -0
- simpserver-0.1.1.post1.dist-info/top_level.txt +1 -0
simpserver/database.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import mariadb
|
|
2
|
+
from os import environ
|
|
3
|
+
|
|
4
|
+
DB_CONFIG = {
|
|
5
|
+
"host": environ.get("DB_HOST", "127.0.0.1"),
|
|
6
|
+
"port": int(environ.get("DB_PORT", 3306)),
|
|
7
|
+
"user": environ.get("DB_USER", "root"),
|
|
8
|
+
"password": environ.get("DB_PASSWORD", "pass"),
|
|
9
|
+
"database": environ.get("DB_NAME", "teste"),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_connection_and_cursor() -> tuple[mariadb.Connection, mariadb.Cursor]:
|
|
14
|
+
conn = mariadb.connect(**DB_CONFIG)
|
|
15
|
+
return conn, conn.cursor()
|
simpserver/exceptions.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class APIError(Exception):
|
|
5
|
+
"""Error handled by api"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, status_code: HTTPStatus, response: dict) -> None:
|
|
8
|
+
self.response = {"error": type(self).__name__} | response
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
super().__init__()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BadRequestError(APIError):
|
|
14
|
+
"""Bad request error"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, response: dict) -> None:
|
|
17
|
+
super().__init__(HTTPStatus.BAD_REQUEST, response)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BodyKeyMissingError(BadRequestError):
|
|
21
|
+
"""When a needed key in body is missing"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, key_name: str) -> None:
|
|
24
|
+
super().__init__({"key_name": key_name})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BodyKeyTypeError(BadRequestError):
|
|
28
|
+
"""When a type of a body key is wrong"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, key_name: str, key_type: type) -> None:
|
|
31
|
+
super().__init__({"key_name": key_name, "type_needed": key_type.__name__})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UrlParamMissingError(BadRequestError):
|
|
35
|
+
"""When a needed param in url is missing"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, param_name: str) -> None:
|
|
38
|
+
super().__init__({"param_name": param_name})
|
|
39
|
+
|
|
40
|
+
class UrlParamTypeError(BadRequestError):
|
|
41
|
+
"""When a needed param in url is missing"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, param_name: str, param_type: type) -> None:
|
|
44
|
+
super().__init__({"param_name": param_name, "type_needed": param_type.__name__})
|
|
45
|
+
|
|
46
|
+
class CredentialsError(APIError):
|
|
47
|
+
"""When a login error occours"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, ) -> None:
|
|
50
|
+
super().__init__(HTTPStatus.NOT_FOUND, {"message": "Wrong credentials"})
|
|
51
|
+
|
|
52
|
+
class InvalidTokenError(APIError):
|
|
53
|
+
"""When a passed token does not exists"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, ) -> None:
|
|
56
|
+
super().__init__(HTTPStatus.UNAUTHORIZED, {"message": "Invalid token"})
|
|
57
|
+
|
|
58
|
+
class TimeoutError(APIError):
|
|
59
|
+
"""When lost much time in an action"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, ) -> None:
|
|
62
|
+
super().__init__(HTTPStatus.REQUEST_TIMEOUT, {"message": "Timeout"})
|
simpserver/handler.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from http import HTTPMethod, HTTPStatus
|
|
2
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
3
|
+
import json
|
|
4
|
+
import urllib.parse
|
|
5
|
+
import traceback
|
|
6
|
+
import signal
|
|
7
|
+
|
|
8
|
+
from .exceptions import APIError
|
|
9
|
+
from .router import RouteCallbackReturn, route_get_callback
|
|
10
|
+
|
|
11
|
+
def timeout_handler(signum, frame):
|
|
12
|
+
raise TimeoutError()
|
|
13
|
+
|
|
14
|
+
signal.signal(signal.SIGALRM, timeout_handler)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RequestHandler(BaseHTTPRequestHandler):
|
|
18
|
+
def __init__(self, request, client_address, server) -> None:
|
|
19
|
+
super().__init__(request, client_address, server)
|
|
20
|
+
|
|
21
|
+
def set_default_headers(self) -> None:
|
|
22
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
23
|
+
self.send_header(
|
|
24
|
+
"Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"
|
|
25
|
+
)
|
|
26
|
+
self.send_header("Access-Control-Allow-Headers", "token, Content-Type")
|
|
27
|
+
self.send_header("Content-Type", "text/html")
|
|
28
|
+
self.end_headers()
|
|
29
|
+
|
|
30
|
+
def get_body(self) -> dict:
|
|
31
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
32
|
+
signal.setitimer(signal.ITIMER_REAL, 0.01, 0)
|
|
33
|
+
body_data = self.rfile.read(content_length).decode("utf-8")
|
|
34
|
+
signal.setitimer(signal.ITIMER_REAL, 0)
|
|
35
|
+
|
|
36
|
+
if not body_data: return {}
|
|
37
|
+
|
|
38
|
+
body = json.loads(body_data)
|
|
39
|
+
|
|
40
|
+
if not isinstance(body, dict): raise ValueError("Invalid body")
|
|
41
|
+
|
|
42
|
+
return body
|
|
43
|
+
|
|
44
|
+
def get_url_params(self) -> dict:
|
|
45
|
+
if "?" not in self.path: return {}
|
|
46
|
+
|
|
47
|
+
url_params = {}
|
|
48
|
+
params_list = self.path.split("?")[1]
|
|
49
|
+
|
|
50
|
+
params_list = params_list.split("&") if "&" in params_list else [params_list]
|
|
51
|
+
|
|
52
|
+
for param in params_list:
|
|
53
|
+
name, value = param.split("=")
|
|
54
|
+
url_params[name] = urllib.parse.unquote_plus(value)
|
|
55
|
+
|
|
56
|
+
return url_params
|
|
57
|
+
|
|
58
|
+
def run_route(self, method: HTTPMethod) -> None:
|
|
59
|
+
path = self.path
|
|
60
|
+
if "?" in path:
|
|
61
|
+
path = path.split("?")[0]
|
|
62
|
+
|
|
63
|
+
route_callback = route_get_callback(path, method)
|
|
64
|
+
|
|
65
|
+
if not route_callback:
|
|
66
|
+
self.send_response(HTTPStatus.NOT_FOUND)
|
|
67
|
+
self.set_default_headers()
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
status_code = HTTPStatus.OK
|
|
71
|
+
response: RouteCallbackReturn
|
|
72
|
+
try:
|
|
73
|
+
# DONT USE POSITIONAL ARGUMENTS, WILL NOT BE PASSED FORWARD.
|
|
74
|
+
# This is just in case you want to add some variables on route_callback.
|
|
75
|
+
# Aways use named parameters like below. Decorators handles just kwargs, not args.
|
|
76
|
+
# If you want to create a decorator, i think that is better keep this pattern.
|
|
77
|
+
response = route_callback(req=self,
|
|
78
|
+
body=self.get_body(),
|
|
79
|
+
url_params=self.get_url_params())
|
|
80
|
+
except APIError as api_error:
|
|
81
|
+
status_code = api_error.status_code
|
|
82
|
+
response = api_error.response
|
|
83
|
+
except:
|
|
84
|
+
status_code = HTTPStatus.INTERNAL_SERVER_ERROR
|
|
85
|
+
print(traceback.format_exc())
|
|
86
|
+
response = {"error": "Internal error"}
|
|
87
|
+
|
|
88
|
+
self.send_response(status_code)
|
|
89
|
+
self.set_default_headers()
|
|
90
|
+
|
|
91
|
+
if isinstance(response, str):
|
|
92
|
+
response = {"message": response}
|
|
93
|
+
|
|
94
|
+
self.wfile.write(json.dumps(response).encode("utf-8"))
|
|
95
|
+
|
|
96
|
+
def do_GET(self) -> None:
|
|
97
|
+
self.run_route(HTTPMethod.GET)
|
|
98
|
+
|
|
99
|
+
def do_POST(self) -> None:
|
|
100
|
+
self.run_route(HTTPMethod.POST)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def serve_api(ip: str, port: int) -> None:
|
|
104
|
+
server = HTTPServer(((ip, port)), RequestHandler)
|
|
105
|
+
|
|
106
|
+
print(f"Running api at: http://{ip}:{port}")
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
server.serve_forever()
|
|
110
|
+
except KeyboardInterrupt:
|
|
111
|
+
pass
|
|
112
|
+
server.server_close()
|
simpserver/router.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from http import HTTPMethod
|
|
2
|
+
from typing import Callable
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from .database import get_connection_and_cursor
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
BodyKeyTypeError,
|
|
8
|
+
BodyKeyMissingError,
|
|
9
|
+
InvalidTokenError,
|
|
10
|
+
UrlParamMissingError,
|
|
11
|
+
UrlParamTypeError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_routes: dict[HTTPMethod, dict[str, Callable]] = {}
|
|
16
|
+
|
|
17
|
+
# Parameters is Any | None.
|
|
18
|
+
RouteCallbackReturn = str | dict | list
|
|
19
|
+
RouteCallback = Callable[..., RouteCallbackReturn]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def route_add(path: str, method: HTTPMethod, callback: RouteCallback) -> None:
|
|
23
|
+
if method not in _routes.keys():
|
|
24
|
+
_routes[method] = {path: callback}
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
if path in _routes[method].keys():
|
|
28
|
+
raise ValueError(f"Trying to add endpoint: {path} but already exists.")
|
|
29
|
+
|
|
30
|
+
_routes[method][path] = callback
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def route_get_callback(path: str, method: HTTPMethod) -> RouteCallback | None:
|
|
34
|
+
if method not in _routes.keys() or path not in _routes[method].keys():
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
return _routes[method][path]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FunctionSignature:
|
|
41
|
+
def __init__(self, func: RouteCallback) -> None:
|
|
42
|
+
self.sig = inspect.signature(func)
|
|
43
|
+
self.params_names = self.sig.parameters.keys()
|
|
44
|
+
self.has_conn = "conn" in self.params_names
|
|
45
|
+
self.has_cur = "cur" in self.params_names
|
|
46
|
+
self.has_conn_or_cur = self.has_conn or self.has_cur
|
|
47
|
+
self.has_req = "req" in self.params_names
|
|
48
|
+
self.has_body = "body" in self.params_names
|
|
49
|
+
self.has_kwargs = "body" in self.params_names
|
|
50
|
+
self.has_url_params = "url_params" in self.params_names
|
|
51
|
+
self.has_user_info = "user_info" in self.params_names
|
|
52
|
+
|
|
53
|
+
unsafe_functions: dict[RouteCallback, FunctionSignature] = {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_func_sig(func: RouteCallback) -> None:
|
|
57
|
+
if func in unsafe_functions.keys():
|
|
58
|
+
print("[WARNIG] - (Re)adding func to func_signatures")
|
|
59
|
+
|
|
60
|
+
func_sig = FunctionSignature(func)
|
|
61
|
+
|
|
62
|
+
for t in func_sig.sig.parameters.values():
|
|
63
|
+
if t.kind == inspect.Parameter.VAR_KEYWORD:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
unsafe_functions[func] = func_sig
|
|
67
|
+
|
|
68
|
+
def safe_run(func: RouteCallback, params: dict) -> RouteCallbackReturn:
|
|
69
|
+
if func not in unsafe_functions.keys():
|
|
70
|
+
return func(**params)
|
|
71
|
+
|
|
72
|
+
func_sig = unsafe_functions[func]
|
|
73
|
+
if not func_sig.has_req: params.pop("req")
|
|
74
|
+
if not func_sig.has_body: params.pop("body")
|
|
75
|
+
if not func_sig.has_url_params: params.pop("url_params")
|
|
76
|
+
if not func_sig.has_user_info: params.pop("user_info", None)
|
|
77
|
+
else:
|
|
78
|
+
if not func_sig.has_conn: params.pop("conn")
|
|
79
|
+
if not func_sig.has_cur: params.pop("cur")
|
|
80
|
+
|
|
81
|
+
# If use cursor or connection, the commit() is executed automatically at the end.
|
|
82
|
+
if func_sig.has_conn_or_cur and not func_sig.has_user_info:
|
|
83
|
+
conn, cur = get_connection_and_cursor()
|
|
84
|
+
|
|
85
|
+
if func_sig.has_conn: params["conn"] = conn
|
|
86
|
+
if func_sig.has_cur: params["cur"] = cur
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
This solves a bug that if get an error with opened connection, the
|
|
90
|
+
server freezes. Why? Because was not closed? I dont know.
|
|
91
|
+
"""
|
|
92
|
+
with conn:
|
|
93
|
+
with cur:
|
|
94
|
+
response = func(**params)
|
|
95
|
+
|
|
96
|
+
conn.commit() # Auto committing the db connection.
|
|
97
|
+
return response
|
|
98
|
+
|
|
99
|
+
return func(**params)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def route(path: str, method: HTTPMethod):
|
|
103
|
+
def decorator(func: RouteCallback):
|
|
104
|
+
def wrapper(**kwargs) -> RouteCallbackReturn:
|
|
105
|
+
return safe_run(func, kwargs)
|
|
106
|
+
|
|
107
|
+
route_add(path, method, wrapper)
|
|
108
|
+
|
|
109
|
+
callback = func
|
|
110
|
+
while callback.__closure__ != None:
|
|
111
|
+
callback = callback.__closure__[0].cell_contents
|
|
112
|
+
|
|
113
|
+
add_func_sig(callback)
|
|
114
|
+
return wrapper
|
|
115
|
+
|
|
116
|
+
return decorator
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def ensure_body_keys(keys: dict[str, type]):
|
|
120
|
+
def decorator(func: RouteCallback):
|
|
121
|
+
def wrapper(**kwargs) -> RouteCallbackReturn:
|
|
122
|
+
body = kwargs["body"]
|
|
123
|
+
body_names = body.keys()
|
|
124
|
+
|
|
125
|
+
for name, t in keys.items():
|
|
126
|
+
if name not in body_names: raise BodyKeyMissingError(name)
|
|
127
|
+
|
|
128
|
+
try: body[name] = t(body[name])
|
|
129
|
+
except: raise BodyKeyTypeError(name, t)
|
|
130
|
+
|
|
131
|
+
return safe_run(func, kwargs)
|
|
132
|
+
|
|
133
|
+
return wrapper
|
|
134
|
+
|
|
135
|
+
return decorator
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def ensure_url_params(params: dict[str, type]):
|
|
139
|
+
def decorator(func: RouteCallback):
|
|
140
|
+
def wrapper(**kwargs) -> RouteCallbackReturn:
|
|
141
|
+
url_params = kwargs["url_params"]
|
|
142
|
+
url_params_names = url_params.keys()
|
|
143
|
+
|
|
144
|
+
for name, t in params.items():
|
|
145
|
+
if name not in url_params_names:
|
|
146
|
+
raise UrlParamMissingError(name)
|
|
147
|
+
try:
|
|
148
|
+
url_params[name] = t(url_params[name])
|
|
149
|
+
except:
|
|
150
|
+
raise UrlParamTypeError(name, t)
|
|
151
|
+
|
|
152
|
+
return safe_run(func, kwargs)
|
|
153
|
+
|
|
154
|
+
return wrapper
|
|
155
|
+
|
|
156
|
+
return decorator
|
|
157
|
+
|
|
158
|
+
def middleware():
|
|
159
|
+
def decorator(func: RouteCallback):
|
|
160
|
+
@ensure_body_keys({"token": str})
|
|
161
|
+
def wrapper(**kwargs) -> RouteCallbackReturn:
|
|
162
|
+
from routes.users import logins
|
|
163
|
+
|
|
164
|
+
if kwargs["body"]["token"] not in logins.keys():
|
|
165
|
+
raise InvalidTokenError()
|
|
166
|
+
|
|
167
|
+
func_sig = unsafe_functions[func]
|
|
168
|
+
|
|
169
|
+
if func_sig.has_user_info:
|
|
170
|
+
conn, cur = get_connection_and_cursor()
|
|
171
|
+
kwargs["conn"] = conn
|
|
172
|
+
kwargs["cur"] = cur
|
|
173
|
+
|
|
174
|
+
cur.execute("SELECT * FROM Users WHERE id = ?", (logins[kwargs["body"]["token"]],))
|
|
175
|
+
row = cur.fetchone()
|
|
176
|
+
|
|
177
|
+
kwargs["user_info"] = {"id": row[0], "name": row[1], "password": row[2]}
|
|
178
|
+
|
|
179
|
+
with conn:
|
|
180
|
+
with cur:
|
|
181
|
+
return safe_run(func, kwargs)
|
|
182
|
+
|
|
183
|
+
return safe_run(func, kwargs)
|
|
184
|
+
|
|
185
|
+
return wrapper
|
|
186
|
+
|
|
187
|
+
return decorator
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simpserver
|
|
3
|
+
Version: 0.1.1.post1
|
|
4
|
+
Summary: A simple solution to create servers http(s).
|
|
5
|
+
Author-email: Manel <manelzaum@icloud.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Faltrenn/simpserver
|
|
8
|
+
Project-URL: Issues, https://github.com/Faltrenn/simpserver/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.14.2
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: mariadb
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# SimpleServer
|
|
18
|
+
|
|
19
|
+
A simple solution to create simples http(s) servers.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
simpserver/database.py,sha256=0UEBGdzizpkqGuOzwMpNL7aPrke5RpDTcb2y5YcInho,447
|
|
2
|
+
simpserver/exceptions.py,sha256=GDrUaGCmu5qMeu88Av73SGpEE8G84Bs0gVJHw0dR_l4,1907
|
|
3
|
+
simpserver/handler.py,sha256=NkmVAKz1dE1BTWohiZfNlMbryUmXLw8YNdad2hqiLf0,3642
|
|
4
|
+
simpserver/router.py,sha256=VPIXWWjeSTlSKA48TwenAgslC6sMeU1NaDVbYeVZBOo,5809
|
|
5
|
+
simpserver-0.1.1.post1.dist-info/licenses/LICENSE,sha256=bWxsLG7aTQwC9KqF0tyl1USrIdJmOuzBWz6GBg7azco,565
|
|
6
|
+
simpserver-0.1.1.post1.dist-info/METADATA,sha256=iJ5t1IJAEERjX3QialzK0PFUwLqqDK0SWbaak9O2GQg,608
|
|
7
|
+
simpserver-0.1.1.post1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
8
|
+
simpserver-0.1.1.post1.dist-info/top_level.txt,sha256=Obyq3A8DpN639jnvDK9ZgFUlvoYqyY_EgNUuYMVpf58,11
|
|
9
|
+
simpserver-0.1.1.post1.dist-info/RECORD,,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2026 Emanuel Alves de Medeiros
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simpserver
|