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 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()
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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