whoopapi 0.0.1__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.
whoopapi/utilities.py ADDED
@@ -0,0 +1,289 @@
1
+ import socket as SOCKET
2
+ import ssl as SSL
3
+ import sys
4
+ import threading as THREADING
5
+ from typing import Callable, List
6
+ from urllib.parse import urlparse
7
+
8
+ from .constants import HttpHeaders, HttpMethods, WebProtocols
9
+ from .logging import LOG_ERROR, LOG_PRETTY
10
+ from .parsers.http_body import parse_body
11
+ from .parsers.http_headers import parse_headers
12
+ from .protocol_handlers.http import RequestHandler, handle_http_client_request
13
+ from .protocol_handlers.websocket import (
14
+ WebsocketHandler,
15
+ handle_websocket_client_request,
16
+ perform_websocket_handshake,
17
+ )
18
+ from .wrappers import HttpRequest
19
+
20
+
21
+ def read_http_client_request_body(socket: SOCKET.socket, body_size: int):
22
+ LOW_THRESHOLD = 1024
23
+ buffer = b""
24
+ if body_size == 0:
25
+ return buffer
26
+
27
+ if body_size < 0:
28
+ body_size = sys.maxsize
29
+
30
+ if body_size <= LOW_THRESHOLD:
31
+ return socket.recv(body_size)
32
+
33
+ else:
34
+ buffer = b""
35
+ bytes_read = 0
36
+ bytes_remaining = body_size
37
+
38
+ while True:
39
+ if bytes_remaining < LOW_THRESHOLD:
40
+ buffer = buffer + socket.recv(bytes_remaining)
41
+
42
+ break
43
+
44
+ else:
45
+ chunk = socket.recv(LOW_THRESHOLD)
46
+ if len(chunk) < LOW_THRESHOLD:
47
+ bytes_remaining = 0
48
+
49
+ buffer = buffer + chunk
50
+ bytes_read = bytes_read + LOW_THRESHOLD
51
+ bytes_remaining = body_size - bytes_read
52
+
53
+ return buffer
54
+
55
+
56
+ def read_http_client_request_headers(socket: SOCKET.socket):
57
+ HEADERS_BREAK = b"\r\n\r\n"
58
+ buffer = b""
59
+ while True:
60
+ data = socket.recv(1)
61
+ if data:
62
+ buffer = buffer + data
63
+ if buffer.endswith(HEADERS_BREAK):
64
+ break
65
+
66
+ return buffer[: -len(HEADERS_BREAK)]
67
+
68
+
69
+ def read_http_client_request(socket: SOCKET.socket):
70
+ try:
71
+ headers_data = read_http_client_request_headers(socket=socket)
72
+ parsed_headers = parse_headers(headers_data)
73
+
74
+ headers = parsed_headers["headers"]
75
+ header_params = parsed_headers["header_params"]
76
+ request_info = parsed_headers["request_info"]
77
+
78
+ if request_info.get("method", "get").lower() == HttpMethods.GET:
79
+ body_size = 0
80
+
81
+ else:
82
+ body_size = int(headers.get(HttpHeaders.CONTENT_LENGTH, -1))
83
+
84
+ body_data = read_http_client_request_body(socket, body_size)
85
+ parsed_body = parse_body(headers, header_params, body_data)
86
+
87
+ http_request = HttpRequest(
88
+ request_info=request_info,
89
+ request_header_params=header_params,
90
+ request_headers=headers,
91
+ request_body=parsed_body,
92
+ )
93
+
94
+ return http_request, None
95
+
96
+ except Exception as e:
97
+ return None, f"{e}"
98
+
99
+
100
+ def parse_route_path(path: str):
101
+ parsed = urlparse(url=path)
102
+ return parsed.path
103
+
104
+
105
+ class Application:
106
+ def __init__(self):
107
+ self.middlewares = []
108
+ self.http_routes = []
109
+ self.websocket_routes = []
110
+ self.ssl_cert_file = None
111
+ self.ssl_key_file = None
112
+
113
+ def set_ssl(self, cert_file: str, key_file: str):
114
+ self.ssl_cert_file = cert_file
115
+ self.ssl_key_file = key_file
116
+
117
+ def add_middleware(self, *middlewares: Callable):
118
+ self.middlewares = [*self.middlewares, *middlewares]
119
+
120
+ def route_http(self, handler: Callable | RequestHandler, path: str):
121
+ self.http_routes = [*self.http_routes, (parse_route_path(path=path), handler)]
122
+
123
+ def route_websocket(self, handler: Callable | WebsocketHandler, path: str):
124
+ self.websocket_routes = [
125
+ *self.websocket_routes,
126
+ (parse_route_path(path=path), handler),
127
+ ]
128
+
129
+ def route(self, path: str, methods: List[str] = None):
130
+ def wrapper_(handler_: Callable):
131
+ if methods:
132
+
133
+ def wrapped_handler_(request_: HttpRequest):
134
+ if request_.method.upper() in methods:
135
+ return handler_(request_)
136
+
137
+ return None
138
+
139
+ self.route_http(handler=wrapped_handler_, path=path)
140
+
141
+ return wrapped_handler_
142
+
143
+ else:
144
+ self.route_http(handler=handler_, path=path)
145
+
146
+ return handler_
147
+
148
+ return wrapper_
149
+
150
+ def route_ws(self, path: str):
151
+ def wrapper_(handler_: Callable | WebsocketHandler):
152
+ self.route_websocket(handler=handler_, path=path)
153
+
154
+ return handler_
155
+
156
+ return wrapper_
157
+
158
+ def clear_middlewares(self):
159
+ self.middlewares = []
160
+
161
+ def clear_http_routes(self):
162
+ self.http_routes = []
163
+
164
+ def clear_websocket_routes(self):
165
+ self.websocket_routes = []
166
+
167
+ def execute_request(self, request: HttpRequest):
168
+ handle_http_client_request(
169
+ request=request, middlewares=self.middlewares, http_routes=self.http_routes
170
+ )
171
+
172
+ def listen(self, port: int | str = 8000, on_start: Callable = None):
173
+ bind_address = ("", int(port))
174
+
175
+ start_application(
176
+ bind_address=bind_address,
177
+ application=self,
178
+ on_start=on_start,
179
+ ssl_cert_file=self.ssl_cert_file,
180
+ ssl_key_file=self.ssl_key_file,
181
+ )
182
+
183
+
184
+ def handle_client_connection(
185
+ socket: SOCKET.socket,
186
+ application: Application,
187
+ ):
188
+ http_request, error = read_http_client_request(socket)
189
+ if error:
190
+ LOG_ERROR(error)
191
+ socket.close()
192
+
193
+ else:
194
+ request_protocol = http_request.protocol.lower()
195
+
196
+ if request_protocol.lower() in [
197
+ WebProtocols.HTTP,
198
+ WebProtocols.HTTPS,
199
+ ]:
200
+ request_headers = http_request.headers
201
+
202
+ connection = request_headers.get(HttpHeaders.CONNECTION, "")
203
+ upgrade = request_headers.get(HttpHeaders.UPGRADE, "")
204
+
205
+ handler_response = None
206
+ if connection and connection.lower() == "upgrade":
207
+ if upgrade and upgrade.lower() == "websocket":
208
+ handshake_successful = perform_websocket_handshake(
209
+ socket, request_headers
210
+ )
211
+ if handshake_successful:
212
+ handle_websocket_client_request(
213
+ socket=socket,
214
+ request=http_request,
215
+ middlewares=application.middlewares,
216
+ websocket_routes=application.websocket_routes,
217
+ )
218
+
219
+ else:
220
+ handler_response = handle_http_client_request(
221
+ request=http_request,
222
+ middlewares=application.middlewares,
223
+ http_routes=application.http_routes,
224
+ )
225
+
226
+ else:
227
+ handler_response = handle_http_client_request(
228
+ request=http_request,
229
+ middlewares=application.middlewares,
230
+ http_routes=application.http_routes,
231
+ )
232
+
233
+ if handler_response:
234
+ result = handler_response.build(request_headers=request_headers)
235
+ socket.sendall(result)
236
+ socket.close()
237
+
238
+ else:
239
+ try:
240
+ socket.close()
241
+
242
+ except Exception as e:
243
+ LOG_ERROR(e)
244
+
245
+ else:
246
+ socket.close()
247
+
248
+
249
+ def start_application(
250
+ bind_address: tuple[str, int],
251
+ application: Application,
252
+ on_start: Callable = None,
253
+ ssl_cert_file: str = None,
254
+ ssl_key_file: str = None,
255
+ ):
256
+ application.http_routes.sort(key=lambda x: len(x[0]), reverse=True)
257
+ application.websocket_routes.sort(key=lambda x: len(x[0]), reverse=True)
258
+
259
+ server_socket = SOCKET.socket(SOCKET.AF_INET, SOCKET.SOCK_STREAM)
260
+ server_socket.bind(bind_address)
261
+ server_socket.listen(1000)
262
+
263
+ if callable(on_start):
264
+ on_start(bind_address)
265
+
266
+ else:
267
+ LOG_ERROR(f"Server running on port {bind_address[1]}")
268
+
269
+ while True:
270
+ client_socket, client_address = server_socket.accept()
271
+ if ssl_cert_file and ssl_key_file:
272
+ ssl_context = SSL.SSLContext(SSL.PROTOCOL_TLS_SERVER)
273
+ ssl_context.load_cert_chain(certfile=ssl_cert_file, keyfile=ssl_key_file)
274
+
275
+ try:
276
+ client_socket = ssl_context.wrap_socket(client_socket, server_side=True)
277
+
278
+ except Exception as e:
279
+ LOG_PRETTY(e)
280
+
281
+ try:
282
+ client_thread = THREADING.Thread(
283
+ target=handle_client_connection,
284
+ args=(client_socket, application),
285
+ )
286
+ client_thread.start()
287
+
288
+ except Exception as e:
289
+ LOG_PRETTY(e)
whoopapi/wrappers.py ADDED
@@ -0,0 +1,193 @@
1
+ import gzip
2
+ import json as JSON
3
+ import zlib
4
+ from typing import Optional
5
+
6
+ import brotli
7
+
8
+ from .constants import (
9
+ HttpContentTypes,
10
+ HttpHeaders,
11
+ HttpStatusCodes,
12
+ get_content_type_from_filename,
13
+ get_default_headers,
14
+ get_http_status_code_message,
15
+ )
16
+
17
+
18
+ class HttpRequestBody:
19
+ def __init__(self, **kwargs):
20
+ self.json = kwargs.get("json", None)
21
+ self.form_data = kwargs.get("form_data", None)
22
+ self.files = kwargs.get("files", None)
23
+ self.raw = kwargs.get("raw", None)
24
+
25
+
26
+ class HttpRequest:
27
+ def __init__(
28
+ self,
29
+ request_info: Optional[dict] = None,
30
+ request_headers: Optional[dict] = None,
31
+ request_header_params: Optional[dict] = None,
32
+ request_body: Optional[dict] = None,
33
+ ):
34
+ request_info = request_info or {}
35
+ self.protocol = request_info.get("protocol", "")
36
+ self.protocol_version = request_info.get("protocol_version", "")
37
+ self.method = request_info.get("method", "")
38
+ self.path = request_info.get("path", "")
39
+ self.query_params = request_info.get("query_params", {})
40
+ self.host = request_headers.get("host", "")
41
+
42
+ self.headers = request_headers or {}
43
+ self.header_params = request_header_params
44
+
45
+ request_body = request_body or {}
46
+ self.body = HttpRequestBody(**request_body)
47
+ self.files = self.body.files
48
+
49
+ self.context = {}
50
+
51
+ def set_context_key(self, key: str, value):
52
+ self.context[key] = value
53
+ return self
54
+
55
+ def update_context(self, update: dict):
56
+ self.context.update(update)
57
+ return self
58
+
59
+ def set_context(self, context: dict):
60
+ self.context = context
61
+ return self
62
+
63
+ def get_context(self):
64
+ return self.context
65
+
66
+ def get_headers(self):
67
+ return self.headers
68
+
69
+
70
+ class HttpResponse:
71
+ def __init__(self):
72
+ self.headers = get_default_headers()
73
+ self.body = b""
74
+ self.http_version = "HTTP/1.1"
75
+ self.status_code = HttpStatusCodes.C_200
76
+ self.DEFAULT_ENCODING = "utf-8"
77
+
78
+ def set_http_version(self, version: str):
79
+ self.http_version = (
80
+ f"HTTP/{version}" if not version.startswith("HTTP") else version
81
+ )
82
+
83
+ return self
84
+
85
+ def set_status_code(self, code: int | str):
86
+ if isinstance(code, str):
87
+ self.status_code = f"{code}"
88
+
89
+ else:
90
+ self.status_code = f"{code} {get_http_status_code_message(code)}"
91
+
92
+ return self
93
+
94
+ def set_header(self, header: str, value: str):
95
+ self.headers[header] = value
96
+
97
+ return self
98
+
99
+ def set_headers(self, headers: dict[str, str]):
100
+ self.headers.update(headers)
101
+
102
+ return self
103
+
104
+ def set_json(self, data: dict | list):
105
+ self.set_header(
106
+ HttpHeaders.CONTENT_TYPE,
107
+ HttpContentTypes.APPLICATION_JSON,
108
+ )
109
+ self.body = bytes(JSON.dumps(data), self.DEFAULT_ENCODING)
110
+
111
+ return self
112
+
113
+ def set_body(self, body: bytes | str):
114
+ self.body = (
115
+ body if isinstance(body, bytes) else bytes(body, self.DEFAULT_ENCODING)
116
+ )
117
+
118
+ return self
119
+
120
+ def set_html(self, html: str):
121
+ self.set_body(html)
122
+ self.set_header(HttpHeaders.CONTENT_TYPE, HttpContentTypes.TEXT_HTML)
123
+
124
+ return self
125
+
126
+ def set_file(self, filename: str, data: bytes, as_attachment: bool = True):
127
+ self.set_body(data)
128
+ self.set_header(
129
+ HttpHeaders.CONTENT_DISPOSITION,
130
+ f"{'attachment' if as_attachment else 'inline'} filename={filename}",
131
+ )
132
+ self.set_header(
133
+ HttpHeaders.CONTENT_TYPE,
134
+ f"{get_content_type_from_filename(filename)}",
135
+ )
136
+
137
+ return self
138
+
139
+ def set_text(self, text: str):
140
+ self.set_body(text)
141
+ self.set_header(HttpHeaders.CONTENT_TYPE, HttpContentTypes.TEXT_PLAIN)
142
+
143
+ return self
144
+
145
+ def build_client_supported_compressions(
146
+ self,
147
+ request_headers: dict = None,
148
+ ):
149
+ accepted_compressions = request_headers.get(HttpHeaders.ACCEPT_ENCODING, "")
150
+
151
+ if accepted_compressions:
152
+ accepted_compressions = [
153
+ t.strip() for t in accepted_compressions.split(",")
154
+ ]
155
+
156
+ if "gzip" in accepted_compressions:
157
+ self.set_header(HttpHeaders.CONTENT_ENCODING, "gzip")
158
+
159
+ return gzip.compress(self.body)
160
+
161
+ elif "deflate" in accepted_compressions:
162
+ self.set_header(HttpHeaders.CONTENT_ENCODING, "deflate")
163
+
164
+ return zlib.compress(self.body)
165
+
166
+ elif "br" in accepted_compressions:
167
+ self.set_header(HttpHeaders.CONTENT_ENCODING, "br")
168
+
169
+ return brotli.compress(self.body)
170
+
171
+ return self.body
172
+
173
+ def build_headers(self):
174
+ items = [f"{self.http_version} {self.status_code}"]
175
+ for key, value in self.headers.items():
176
+ items.append(f"{key}: {value}")
177
+
178
+ return bytes("\r\n".join(items), self.DEFAULT_ENCODING)
179
+
180
+ def build(self, request_headers: dict = None):
181
+ body = (
182
+ self.build_client_supported_compressions(request_headers=request_headers)
183
+ if request_headers
184
+ else self.body
185
+ )
186
+
187
+ result = b""
188
+ self.set_header(HttpHeaders.CONTENT_LENGTH, str(len(body)))
189
+ result = result + self.build_headers()
190
+ result = result + b"\r\n\r\n"
191
+ result = result + body
192
+
193
+ return result
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: whoopapi
3
+ Version: 0.0.1
4
+ Summary: A lightweight web API framework.
5
+ Author-email: Derrick Wafula <derrickwafula@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/DerrohXy/WhoopAPI
8
+ Project-URL: Issues, https://github.com/DerrohXy/WhoopAPI/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: brotli>=1.1.0
15
+ Dynamic: license-file
16
+
17
+ # Simple lightweight web API framework
@@ -0,0 +1,17 @@
1
+ whoopapi/__init__.py,sha256=--0GIIKpi_qAlGEwSW6yH8kRgrClj7jkrMFBvUAYvy8,308
2
+ whoopapi/constants.py,sha256=mhq7BkL44CUoJzJ-n-iKz_xi3l4veg6u5uzgAowM6uA,11857
3
+ whoopapi/logging.py,sha256=KHoPlodJuZt2mi_MtA0vA1LrO03MRc_Z7ARgtlhroac,267
4
+ whoopapi/responses.py,sha256=YuIPsXWc5iTFdqjvkY-74HKiwPJT9eH1r5RaiTwnfqo,386
5
+ whoopapi/utilities.py,sha256=VAuyc8cqC6hFyAxna9Yx7NmwlTkl5g2y1VouZ0JG3yE,8711
6
+ whoopapi/wrappers.py,sha256=oPfoUJO7ku8A9u_v0ijKUryYOHfK1MAGcCVe-ogWUhE,5481
7
+ whoopapi/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ whoopapi/parsers/http_body.py,sha256=wO3Fs_hSyVL6k1BOrSi7K9ol8IO6lJ557gQTEDiekho,8317
9
+ whoopapi/parsers/http_headers.py,sha256=sVCNgAe66M4IFNfw-z10_uvTO5ck40rxcUDnQ0NpPgE,5278
10
+ whoopapi/protocol_handlers/__init__.py,sha256=42tgIMu4D4EGxwlUH6O_FdCyZXALlQhO_CLizZRGZck,107
11
+ whoopapi/protocol_handlers/http.py,sha256=wWiGM86cnTUCzXx6WTRC8WLqjaTVdBStkpQYWuD7kGk,4926
12
+ whoopapi/protocol_handlers/websocket.py,sha256=q3q1YtYufKl5TtPeWIJPAYCU8Mjg-qWOFFxtJlK9pHA,6421
13
+ whoopapi-0.0.1.dist-info/licenses/LICENSE,sha256=7w1_gfAIaRj6QAY0GKY1P-Q1J9ycz90QOl7gIzEESms,1072
14
+ whoopapi-0.0.1.dist-info/METADATA,sha256=dGHwwNp5bt3pEI-j74XgWJDWtKsahPYAdF--VRsFhgM,569
15
+ whoopapi-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ whoopapi-0.0.1.dist-info/top_level.txt,sha256=QuIAitoyEjEvjZVujGbDfWEb08tGXnNBm2ttm_TvPXI,9
17
+ whoopapi-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Derrick Wafula
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1 @@
1
+ whoopapi