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/__init__.py +8 -0
- whoopapi/constants.py +319 -0
- whoopapi/logging.py +21 -0
- whoopapi/parsers/__init__.py +0 -0
- whoopapi/parsers/http_body.py +251 -0
- whoopapi/parsers/http_headers.py +160 -0
- whoopapi/protocol_handlers/__init__.py +3 -0
- whoopapi/protocol_handlers/http.py +162 -0
- whoopapi/protocol_handlers/websocket.py +239 -0
- whoopapi/responses.py +11 -0
- whoopapi/utilities.py +289 -0
- whoopapi/wrappers.py +193 -0
- whoopapi-0.0.1.dist-info/METADATA +17 -0
- whoopapi-0.0.1.dist-info/RECORD +17 -0
- whoopapi-0.0.1.dist-info/WHEEL +5 -0
- whoopapi-0.0.1.dist-info/licenses/LICENSE +22 -0
- whoopapi-0.0.1.dist-info/top_level.txt +1 -0
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,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
|