tina4-python 0.2.122__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.
Files changed (47) hide show
  1. tina4_python/Auth.py +222 -0
  2. tina4_python/Constant.py +43 -0
  3. tina4_python/Database.py +591 -0
  4. tina4_python/DatabaseResult.py +107 -0
  5. tina4_python/DatabaseTypes.py +15 -0
  6. tina4_python/Debug.py +126 -0
  7. tina4_python/Env.py +37 -0
  8. tina4_python/Localization.py +42 -0
  9. tina4_python/Messages.py +30 -0
  10. tina4_python/MiddleWare.py +90 -0
  11. tina4_python/Migration.py +107 -0
  12. tina4_python/ORM.py +639 -0
  13. tina4_python/Queue.py +615 -0
  14. tina4_python/Request.py +19 -0
  15. tina4_python/Response.py +121 -0
  16. tina4_python/Router.py +423 -0
  17. tina4_python/Session.py +342 -0
  18. tina4_python/ShellColors.py +20 -0
  19. tina4_python/Swagger.py +228 -0
  20. tina4_python/Template.py +107 -0
  21. tina4_python/Webserver.py +429 -0
  22. tina4_python/Websocket.py +49 -0
  23. tina4_python/__init__.py +392 -0
  24. tina4_python/messages.pot +83 -0
  25. tina4_python/public/css/readme.md +0 -0
  26. tina4_python/public/favicon.ico +0 -0
  27. tina4_python/public/images/403.png +0 -0
  28. tina4_python/public/images/404.png +0 -0
  29. tina4_python/public/images/500.png +0 -0
  30. tina4_python/public/images/logo.png +0 -0
  31. tina4_python/public/images/readme.md +0 -0
  32. tina4_python/public/js/readme.md +0 -0
  33. tina4_python/public/js/reconnecting-websocket.js +365 -0
  34. tina4_python/public/js/tina4helper.js +397 -0
  35. tina4_python/public/swagger/index.html +90 -0
  36. tina4_python/public/swagger/oauth2-redirect.html +63 -0
  37. tina4_python/templates/errors/403.twig +10 -0
  38. tina4_python/templates/errors/404.twig +10 -0
  39. tina4_python/templates/errors/500.twig +11 -0
  40. tina4_python/templates/readme.md +1 -0
  41. tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  42. tina4_python/translations/en/LC_MESSAGES/messages.po +80 -0
  43. tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  44. tina4_python/translations/fr/LC_MESSAGES/messages.po +84 -0
  45. tina4_python-0.2.122.dist-info/METADATA +465 -0
  46. tina4_python-0.2.122.dist-info/RECORD +47 -0
  47. tina4_python-0.2.122.dist-info/WHEEL +4 -0
@@ -0,0 +1,429 @@
1
+ #
2
+ # Tina4 - This is not a 4ramework.
3
+ # Copy-right 2007 - current Tina4
4
+ # License: MIT https://opensource.org/licenses/MIT
5
+ #
6
+ # flake8: noqa: E501
7
+ import asyncio
8
+ import base64
9
+ import json
10
+ import os
11
+ import re
12
+ import mimetypes
13
+ from urllib.parse import unquote_plus
14
+ from urllib.parse import urlparse, parse_qsl
15
+ import tina4_python
16
+ from tina4_python import Constant
17
+ from tina4_python.Constant import HTTP_REDIRECT, HTTP_OK, HTTP_SERVER_ERROR
18
+ from tina4_python.Session import Session
19
+ from tina4_python.Router import Router
20
+ from tina4_python.Debug import Debug
21
+ from tina4_python.Template import Template
22
+
23
+
24
+ def is_int(v):
25
+ try:
26
+ int(v)
27
+ except ValueError:
28
+ return False
29
+ return True
30
+
31
+
32
+ class Webserver:
33
+ async def get_content_length(self):
34
+ # get the content length
35
+ if "content-length" in self.lowercase_headers:
36
+ return int(self.lowercase_headers["content-length"])
37
+
38
+ return 0
39
+
40
+ async def get_content_body(self, content_length):
41
+ # get lines of content where at the end of the request
42
+ content = self.content_raw
43
+
44
+ if "content-type" in self.lowercase_headers:
45
+ if self.lowercase_headers["content-type"] == "application/x-www-form-urlencoded":
46
+ body = {}
47
+ content_data = content.decode("utf-8").split("&")
48
+ for data in content_data:
49
+ data = data.split("=", 1)
50
+ body[data[0]] = unquote_plus(data[1])
51
+ return body, {}
52
+ elif self.lowercase_headers["content-type"] == "application/json":
53
+ # print("CONTENT", content, self.request)
54
+ try:
55
+ return json.loads(content), {}
56
+ except Exception:
57
+ return content.decode("utf-8"), {}
58
+
59
+ elif self.lowercase_headers["content-type"] == "text/plain":
60
+ return content.decode("utf-8"), {}
61
+ else:
62
+ content_data = self.lowercase_headers["content-type"].split("; ")
63
+
64
+ if content_data[0] == "multipart/form-data":
65
+ boundary = content_data[1].split("=")[1] + "\r\n"
66
+ content = b"\r\n" + content
67
+ data_array = content.split(str.encode(boundary))
68
+ body = {}
69
+ files = {}
70
+ for data in data_array:
71
+ data = data.split(b"\r\n\r\n")
72
+ data_names = data[0].decode("utf-8").split("; ")
73
+
74
+ if data_names[0] == "Content-Disposition: form-data":
75
+ key_name = data_names[1].split("=")[1][1:-1]
76
+
77
+ if len(data_names) == 2:
78
+ data_value = data[1].split(b"\r\n")[0]
79
+ body[key_name] = unquote_plus(data_value.decode("utf-8"))
80
+ else:
81
+ data_value = data[1].split(b"\r\n--")[0]
82
+ file_data = data_names[2].split("\r\n")
83
+ file_name = "Unknown"
84
+ content_type = "Unknown"
85
+ meta_data = {}
86
+ for file_info in file_data:
87
+ file_info1 = file_info.split("=")
88
+ if len(file_info1) > 1:
89
+ meta_data[file_info1[0]] = file_info1[1].strip()
90
+ file_info2 = file_info.split(":")
91
+ if len(file_info2) > 1:
92
+ meta_data[file_info2[0]] = file_info2[1].strip()
93
+
94
+ if "filename" in meta_data:
95
+ file_name = meta_data["filename"][1:-1]
96
+ if "Content-Type" in meta_data:
97
+ content_type = meta_data["Content-Type"]
98
+
99
+ if key_name in body:
100
+ body[key_name] = [body[key_name]]
101
+ body[key_name].append({"file_name": file_name, "content_type": content_type,"content": base64.encodebytes(data_value).decode("utf-8").replace(
102
+ "\n", "")})
103
+ else:
104
+ body[key_name] = {"file_name": file_name, "content_type": content_type,"content": base64.encodebytes(data_value).decode("utf-8").replace(
105
+ "\n", "")}
106
+
107
+ if key_name in files:
108
+ files[key_name] = [files[key_name]]
109
+ files[key_name].append({"file_name": file_name, "content_type": content_type,"content": base64.encodebytes(data_value).decode("utf-8").replace(
110
+ "\n", "")})
111
+ else:
112
+ files[key_name] = {"file_name": file_name, "content_type": content_type,"content": base64.encodebytes(data_value).decode("utf-8").replace(
113
+ "\n", "")}
114
+
115
+ return body, files
116
+
117
+ return {"data": base64.encodebytes(content).decode("utf-8").replace("\n", "")}, {}
118
+
119
+ async def send_basic_headers(self, headers):
120
+ self.send_header("Access-Control-Allow-Origin", "*", headers)
121
+ self.send_header("Access-Control-Allow-Headers",
122
+ "Origin, X-Requested-With, Content-Type, Accept, Authorization", headers)
123
+ self.send_header("Access-Control-Allow-Credentials", "True", headers)
124
+ # self.send_header("Content-Length", str(len(response.content)), headers)
125
+ self.send_header("Connection", "Keep-Alive", headers)
126
+ self.send_header("Keep-Alive", "timeout=5, max=30", headers)
127
+
128
+ async def get_response(self, method, transport, asgi_response=False):
129
+ """
130
+ Get response
131
+ :param method: GET, POST, PATCH, DELETE, PUT
132
+ :return:
133
+ """
134
+ headers = []
135
+ if method == "OPTIONS":
136
+ self.send_header("Access-Control-Allow-Origin", "*", headers)
137
+ self.send_header("Access-Control-Allow-Headers",
138
+ "Origin, X-Requested-With, Content-Type, Accept, Authorization", headers)
139
+ self.send_header("Access-Control-Allow-Credentials", "True", headers)
140
+
141
+ headers = await self.get_headers(headers, self.response_protocol, Constant.HTTP_OK)
142
+
143
+ if asgi_response:
144
+ return None, headers
145
+ else:
146
+ return headers
147
+
148
+ params = dict(parse_qsl(urlparse(self.path).query, keep_blank_values=True))
149
+
150
+ new_params = {}
151
+ new_params.update(params)
152
+
153
+ for key, value in params.items():
154
+ regex = r"(\w+)"
155
+ matches = re.finditer(regex, key)
156
+
157
+ var_names = []
158
+ for matchNum, match in enumerate(matches, start=0):
159
+ if is_int(match.group()):
160
+ var_names.append(int(match.group()))
161
+ else:
162
+ var_names.append(match.group())
163
+
164
+ if len(var_names) > 1:
165
+ start_var = new_params
166
+ counter = 0
167
+ while counter < len(var_names):
168
+ var_name = var_names[counter]
169
+ if not is_int(var_name):
170
+ if isinstance(start_var, dict) and var_name in start_var:
171
+ start_var = start_var[var_name]
172
+ else:
173
+ if counter + 1 < len(var_names) and is_int(var_names[counter + 1]):
174
+ if var_name not in start_var:
175
+ start_var[var_name] = []
176
+ start_var = start_var[var_name]
177
+ else:
178
+ if counter - 1 > 0 and is_int(var_names[counter - 1]):
179
+ index = int(var_names[counter - 1])
180
+ new_value = {var_name: value}
181
+ if index in range(len(start_var)):
182
+ start_var[index].update(new_value)
183
+ else:
184
+ while len(start_var) < index:
185
+ start_var.append({})
186
+ start_var.append(new_value)
187
+ start_var = start_var[index]
188
+ else:
189
+ if isinstance(start_var, dict):
190
+ if counter + 1 == len(var_names):
191
+ start_var[var_name] = value
192
+ else:
193
+ start_var[var_name] = {}
194
+ start_var = start_var[var_name]
195
+
196
+ counter += 1
197
+
198
+ params.update(new_params)
199
+
200
+ content_length = await self.get_content_length()
201
+ if method != Constant.TINA4_GET:
202
+ body, files = await self.get_content_body(content_length)
203
+ else:
204
+ body = None
205
+ files = None
206
+
207
+ request = {"params": params, "body": body, "files": files, "raw_data": self.request, "url": self.path, "session": self.session,
208
+ "headers": self.lowercase_headers, "raw_request": self.request_raw, "raw_content": self.content_raw,
209
+ "transport": transport, "asgi_response": asgi_response}
210
+
211
+ tina4_python.tina4_current_request = request
212
+
213
+ response = await self.router_handler.resolve(method, self.path, request, self.lowercase_headers, self.session)
214
+
215
+ if HTTP_REDIRECT != response.http_code:
216
+ self.send_header("Content-Type", response.content_type, headers)
217
+ await self.send_basic_headers(headers)
218
+
219
+ if os.getenv("TINA4_SESSION", "PY_SESS") in self.cookies:
220
+ self.send_header("Set-Cookie",
221
+ os.getenv("TINA4_SESSION", "PY_SESS") + '=' + self.cookies[
222
+ os.getenv("TINA4_SESSION", "PY_SESS")], headers)
223
+
224
+ # add the custom headers from the response
225
+ for response_header in response.headers:
226
+ self.send_header(response_header, response.headers[response_header], headers)
227
+
228
+ if asgi_response:
229
+ return response, headers
230
+
231
+ headers = await self.get_headers(headers, self.response_protocol, response.http_code)
232
+
233
+ if isinstance(response.content, str):
234
+ return headers + response.content.encode()
235
+ else:
236
+ return headers + response.content
237
+
238
+ @staticmethod
239
+ def send_header(header, value, headers):
240
+ headers.append(header + ": " + value)
241
+
242
+ @staticmethod
243
+ async def get_headers(response_headers, response_protocol, response_code):
244
+ headers = response_protocol + " " + str(response_code) + " " + Constant.LOOKUP_HTTP_CODE[
245
+ response_code] + "\r\n"
246
+ for header in response_headers:
247
+ headers += header + "\r\n"
248
+ headers += "\r\n"
249
+
250
+ return headers.encode()
251
+
252
+ async def run_server(self):
253
+ self.server = await asyncio.start_server(self.handle_client, self.host_name, self.port)
254
+ await self.server.serve_forever()
255
+
256
+ async def get_data(self, reader):
257
+ try:
258
+ raw_data = await reader.readuntil(b"\r\n\r\n")
259
+ except Exception:
260
+ raw_data = await reader.read(128)
261
+
262
+ protocol = raw_data.decode("utf-8").split("\r\n", 1)[0]
263
+ header_array = raw_data.decode("utf-8").split("\r\n\r\n")[0]
264
+ header_array = header_array.split("\r\n")
265
+ headers = {}
266
+ for header in header_array:
267
+ split = header.split(":", 1)
268
+ if len(split) == 2:
269
+ headers[split[0]] = split[1].strip()
270
+
271
+ lowercase_headers = {k.lower(): v for k, v in headers.items()}
272
+ content = ""
273
+ content_data = b''
274
+ if "content-length" in lowercase_headers:
275
+ content_length = int(lowercase_headers["content-length"])
276
+ count = 0
277
+ read_size = 64
278
+ content_data = b''
279
+ while len(content_data) < content_length:
280
+ read = await reader.read(read_size)
281
+ count += len(read)
282
+ content_data += read
283
+ raw_data += read
284
+ # print('COUNT', count, len(read))
285
+ if len(read) < read_size and len(content_data) == content_length:
286
+ break
287
+ try:
288
+ content = content_data.decode("utf-8")
289
+ except Exception: # probably binary or multipart form?
290
+ content = content_data
291
+
292
+ return protocol, headers, lowercase_headers, content, raw_data, content_data
293
+
294
+ async def handle_client(self, reader, writer):
295
+ try:
296
+ # Get the client request
297
+ protocol, headers_list, lowercase_headers, request, request_raw, content_raw = await self.get_data(reader)
298
+ # Strange blank request ?
299
+ if protocol == '':
300
+ return
301
+ # Decode the request
302
+ self.request_raw = request_raw
303
+ self.content_raw = content_raw
304
+ self.request = request
305
+ self.headers = headers_list
306
+ self.lowercase_headers = lowercase_headers
307
+
308
+ protocol = protocol.split(" ")
309
+ # print(protocol, headers_list)
310
+ self.method = protocol[0]
311
+ self.path = protocol[1]
312
+
313
+
314
+ method_list = [Constant.TINA4_GET, Constant.TINA4_DELETE, Constant.TINA4_PUT, Constant.TINA4_ANY,
315
+ Constant.TINA4_POST, Constant.TINA4_PATCH, Constant.TINA4_OPTIONS]
316
+
317
+ contains_method = [ele for ele in method_list if (ele in self.method)]
318
+
319
+ request_handled = False
320
+ # return static content asap
321
+ if self.method == "GET" and "sec-websocket-key" not in self.lowercase_headers:
322
+ # split URL and extract query string
323
+ url = Router.clean_url(self.path)
324
+ url_parts = url.split('?')
325
+ url = url_parts[0]
326
+
327
+ # Serve statics
328
+ static_file = tina4_python.root_path + os.sep + "src" + os.sep + "public" + url.replace("/", os.sep)
329
+ Debug.info("Attempting to serve static file: " + static_file)
330
+ if os.path.isfile(static_file):
331
+ mime_type = mimetypes.guess_type(url)[0]
332
+ with open(static_file, 'rb') as file:
333
+ headers = []
334
+ self.send_header("Content-Type", mime_type, headers)
335
+ await self.send_basic_headers(headers)
336
+ content = file.read()
337
+ headers = await self.get_headers(headers, self.response_protocol, HTTP_OK)
338
+ writer.write(headers + content)
339
+ await writer.drain()
340
+ if writer is not None:
341
+ writer.close()
342
+ request_handled = True
343
+
344
+ if not request_handled:
345
+ # parse cookies
346
+ cookie_list = {}
347
+ content = ""
348
+ if "cookie" in self.lowercase_headers:
349
+ cookie_list_temp = self.lowercase_headers["cookie"].split(";")
350
+ for cookie_value in cookie_list_temp:
351
+ cookie = cookie_value.split("=", 1)
352
+ cookie_list[cookie[0].strip()] = cookie[1].strip()
353
+
354
+ self.cookies = cookie_list
355
+
356
+ # initialize the session
357
+ self.session = Session(os.getenv("TINA4_SESSION", "PY_SESS"),
358
+ os.getenv("TINA4_SESSION_FOLDER", tina4_python.root_path + os.sep + "sessions"),
359
+ os.getenv("TINA4_SESSION_HANDLER", "SessionFileHandler")
360
+ )
361
+
362
+ if os.getenv("TINA4_SESSION", "PY_SESS") in self.cookies:
363
+ self.session.load(self.cookies[os.getenv("TINA4_SESSION", "PY_SESS")])
364
+ else:
365
+ self.cookies[os.getenv("TINA4_SESSION", "PY_SESS")] = self.session.start()
366
+
367
+ if "sec-websocket-key" not in self.lowercase_headers:
368
+ try:
369
+ if self.method != "" and contains_method:
370
+ content = await (self.get_response(self.method, writer))
371
+ if content != "":
372
+ writer.write(content)
373
+ await writer.drain()
374
+ writer.close()
375
+ except BrokenPipeError as e:
376
+ Debug.warning("Socket connection broken: " + str(e))
377
+ # socket got terminated
378
+ pass
379
+ else:
380
+ # for sockets
381
+ await (self.get_response(self.method, writer))
382
+
383
+
384
+ except Exception as e:
385
+ error_string = tina4_python.global_exception_handler(e)
386
+ headers = []
387
+ await self.send_basic_headers(headers)
388
+ headers = await self.get_headers(headers, self.response_protocol, HTTP_SERVER_ERROR)
389
+ url = Router.clean_url(self.path)
390
+
391
+ content_type = "text/html"
392
+ if "content-type" in self.lowercase_headers:
393
+ content_type = self.lowercase_headers["content-type"].lower()
394
+
395
+ if content_type == "application/json":
396
+ html = json.dumps({"error": "500 - Internal Server Error",
397
+ "data": {"server": {"url": url}, "error_message": error_string}})
398
+ else:
399
+ html = Template.render_twig_template("errors/500.twig",
400
+ {"server": {"url": url}, "error_message": error_string})
401
+
402
+ writer.write(headers + html.encode())
403
+ await writer.drain()
404
+ writer.close()
405
+
406
+ def __init__(self, host_name, port):
407
+ self.content_raw = None
408
+ self.session = Session
409
+ self.cookies = {}
410
+ self.method = None
411
+ self.response_protocol = "HTTP/1.1"
412
+ self.headers = None
413
+ self.lowercase_headers = None
414
+ self.request = None
415
+ self.request_raw = None
416
+ self.path = None
417
+ self.server_socket = None
418
+ self.host_name = host_name
419
+ self.port = port
420
+ self.router_handler = None
421
+ self.running = False
422
+ self.server = None
423
+
424
+ async def serve_forever(self):
425
+ await self.run_server()
426
+
427
+ def server_close(self):
428
+ self.running = False
429
+ self.server_socket.close()
@@ -0,0 +1,49 @@
1
+ #
2
+ # Tina4 - This is not a 4ramework.
3
+ # Copy-right 2007 - current Tina4
4
+ # License: MIT https://opensource.org/licenses/MIT
5
+ #
6
+ # flake8: noqa: E501
7
+ import importlib
8
+ import os
9
+ from asyncio.trsock import TransportSocket
10
+
11
+ from tina4_python.Debug import Debug
12
+
13
+ class Websocket:
14
+ """
15
+ Websocket class which wraps simple_websocket library
16
+ """
17
+ def __init__(self, request):
18
+ try:
19
+ self.server = importlib.import_module("simple_websocket")
20
+ self.request = request
21
+
22
+ except Exception as e:
23
+ Debug.error("Error creating Websocket, perhaps you need to install simple_websocket ?", e)
24
+
25
+ async def connection(self):
26
+ """
27
+ Returns a websocket connection
28
+ :return:
29
+ """
30
+ try:
31
+ if self.request.asgi_response:
32
+ connection = await self.server.AioServer.accept(asgi=self.request.transport)
33
+ else:
34
+ if os.name == "nt":
35
+ connection = await self.server.AioServer.accept(
36
+ sock=TransportSocket(self.request.transport.transport._sock), # not working properly
37
+ headers=self.request.headers
38
+ )
39
+ else:
40
+ connection = await self.server.AioServer.accept(
41
+ sock=self.request.transport.get_extra_info('socket').dup(),
42
+ headers=self.request.headers
43
+ )
44
+
45
+ return connection
46
+
47
+ except Exception as e:
48
+ Debug.error("Could not establish a socket connection:", str(e))
49
+ return None