diff options
Diffstat (limited to 'day9/task5_vue/backend')
| -rw-r--r-- | day9/task5_vue/backend/core.py | 82 | ||||
| -rw-r--r-- | day9/task5_vue/backend/database.py | 43 | ||||
| -rw-r--r-- | day9/task5_vue/backend/router.py | 41 | ||||
| -rw-r--r-- | day9/task5_vue/backend/server.py | 73 | ||||
| -rw-r--r-- | day9/task5_vue/backend/utils.py | 131 |
5 files changed, 370 insertions, 0 deletions
diff --git a/day9/task5_vue/backend/core.py b/day9/task5_vue/backend/core.py new file mode 100644 index 0000000..71c6d5a --- /dev/null +++ b/day9/task5_vue/backend/core.py @@ -0,0 +1,82 @@ +from abc import ABC, abstractmethod +import json + +from backend.utils import HTTP_STATUS_CODES + + +class Response(ABC): + @property + def status_code(self) -> int: + """ + По дефолту возвращается статус 200 + """ + return 200 + + @property + @abstractmethod + def content_type(self) -> str: + pass + + @property + @abstractmethod + def content(self) -> bytes: + pass + + +class HtmlResponse(Response): + def __init__(self, html, status_code=200): + self._html: str = html + self._code = status_code + + @property + def status_code(self) -> int: + return self._code + + @property + def content_type(self) -> str: + return 'text/html' + + @property + def content(self) -> bytes: + return self._html.encode() + + +class TextFileResponse(Response): + def __init__(self, path, extension): + with open(path, 'rb') as f: + self._content = f.read() + + self._extension = extension + + @property + def content_type(self) -> str: + return f'text/{self._extension}' + + @property + def content(self) -> bytes: + return self._content + + +class ImageResponse(TextFileResponse): + @property + def content_type(self) -> str: + return f'image/{self._extension}' + + +class JsonResponse(Response): + def __init__(self, json_object): + self._json_str = json.dumps(json_object, ensure_ascii=False) + + @property + def content_type(self) -> str: + return 'application/json' + + @property + def content(self) -> bytes: + return self._json_str.encode() + + +class ErrorResponse(HtmlResponse): + def __init__(self, http_code, message=''): + html = f'<center><h1>ERROR {http_code} {HTTP_STATUS_CODES[http_code].upper()}</h1></center><br>{message}' + super().__init__(html, http_code) diff --git a/day9/task5_vue/backend/database.py b/day9/task5_vue/backend/database.py new file mode 100644 index 0000000..5484041 --- /dev/null +++ b/day9/task5_vue/backend/database.py @@ -0,0 +1,43 @@ +import MySQLdb +from backend.config import * + +import logging + + +def db_column_names(): + cursor = db.cursor() + cursor.execute('DESCRIBE table_task1;') + table_structure = cursor.fetchall() + table_headers = [field[0] for field in table_structure] + return table_headers + + +logger = logging.getLogger('tableApp') +logger.info(f'Trying to connect to database "{DATABASE_NAME}@{HOST}"...') +# В файле config.py создайте соответствующие переменные +db = MySQLdb.connect( + host=HOST, + user=USERNAME, + passwd=PASSWORD, + db=DATABASE_NAME +) +logger.info('Connected') + +logger.info(f'Preparing table "table_task1"...') +db.cursor().execute( + ''' + CREATE TABLE IF NOT EXISTS `table_task1` ( + `service_id` int(11) NOT NULL AUTO_INCREMENT, + `servtype` varchar(20) NOT NULL DEFAULT 'hosting', + `subtype` varchar(32) NOT NULL DEFAULT '', + `user_id` bigint(20) NOT NULL, + `referrer_user_id` bigint(20) NOT NULL, + `state` varchar(1) NOT NULL DEFAULT 'N', + `creation_date` date NOT NULL DEFAULT '0000-01-01', + `creation_time` time NOT NULL DEFAULT '00:00:00', + `creation_request_sent_date` datetime DEFAULT NULL, + `notified_about_expiration` smallint(6) NOT NULL DEFAULT '0', + PRIMARY KEY (`service_id`) + ) ENGINE=InnoDB AUTO_INCREMENT=35109400 DEFAULT CHARSET=utf8; + ''' +) diff --git a/day9/task5_vue/backend/router.py b/day9/task5_vue/backend/router.py new file mode 100644 index 0000000..be8305e --- /dev/null +++ b/day9/task5_vue/backend/router.py @@ -0,0 +1,41 @@ +import re +from backend.utils import NOT_FOUND_CODE, BAD_REQUEST_CODE +from backend.core import Response, ErrorResponse + + +def route(url_format, methods=None): + if methods is None: + methods = ['GET'] + + def wrapper(func): + def inner(url, query, *args, **kwargs): + pattern = re.compile(url_format) + match = re.fullmatch(pattern, url) + + if match is None or len(match.groups()) != pattern.groups: + return ErrorResponse(BAD_REQUEST_CODE) + + return func(query, *match.groups(), *args, **kwargs) + + _router_tree[url_format] = _router_tree.get(url_format, {}) + for method in methods: + _router_tree[url_format][method] = inner + + return inner + + return wrapper + + +def run(request) -> Response: + res = ErrorResponse(NOT_FOUND_CODE) + + method, url = request['method'], request['url'] + for url_pattern in _router_tree: + if re.fullmatch(url_pattern, url) and method in _router_tree[url_pattern]: + res = _router_tree[url_pattern][method](url, request['query']) + break + + return res + + +_router_tree = {} diff --git a/day9/task5_vue/backend/server.py b/day9/task5_vue/backend/server.py new file mode 100644 index 0000000..48d5946 --- /dev/null +++ b/day9/task5_vue/backend/server.py @@ -0,0 +1,73 @@ +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import parse_qs + +from backend.router import run +from backend.utils import parse_multipart_form + +import logging +import json + + +class MyHTTPRequestHandler(BaseHTTPRequestHandler): + def _set_response(self, code, content_type): + self.send_response(code) + self.send_header('Content-type', content_type) + self.end_headers() + + def do_GET(self): + try: + content_length = int(self.headers['Content-Length']) + get_data = parse_qs(self.rfile.read(content_length).decode('utf-8')) + except TypeError: + get_data = {} + + for key in get_data: + get_data[key] = get_data[key][0] + + self.finalize_request(run({ + 'url': self.path, + 'method': 'GET', + 'query': get_data + })) + + def do_POST(self): + content_length = int(self.headers['Content-Length']) + content_type = self.headers['Content-type'] + post_data = self.rfile.read(content_length) + + if content_type.split(';')[0] == 'multipart/form-data': + files = parse_multipart_form(post_data) + self.finalize_request(run({ + 'url': self.path, + 'method': 'POST', + 'query': {'files': files} + })) + + elif content_type.split(';')[0] in ['text/plain', 'application/x-www-form-urlencoded']: + post_data = parse_qs(post_data.decode('utf-8')) + for key in post_data: + post_data[key] = post_data[key][0] + + self.finalize_request(run({ + 'url': self.path, + 'method': 'POST', + 'query': post_data + })) + + elif content_type.split(';')[0] == 'application/json': + self.finalize_request(run({ + 'url': self.path, + 'method': 'POST', + 'query': json.loads(post_data.decode('utf-8')) + })) + + def finalize_request(self, response): + self._set_response(response.status_code, response.content_type) + self.wfile.write(response.content) + + +def start_server(host, port): + server_address = (host, port) + httpd = HTTPServer(server_address, MyHTTPRequestHandler) + logging.getLogger('tableApp').info(f'Server started on {host}:{port}') + httpd.serve_forever() diff --git a/day9/task5_vue/backend/utils.py b/day9/task5_vue/backend/utils.py new file mode 100644 index 0000000..c1b3a04 --- /dev/null +++ b/day9/task5_vue/backend/utils.py @@ -0,0 +1,131 @@ +from io import BytesIO + + +CHUNK_SIZE = 49600 + +HTTP_STATUS_CODES = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi Status", + 226: "IM Used", # see RFC 3229 + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", # unused + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", # see RFC 2324 + 421: "Misdirected Request", # see RFC 7540 + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 426: "Upgrade Required", + 428: "Precondition Required", # see RFC 6585 + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 449: "Retry With", # proprietary MS extension + 451: "Unavailable For Legal Reasons", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 507: "Insufficient Storage", + 510: "Not Extended", +} + +SUCCESS_CODE = 200 +BAD_REQUEST_CODE = 400 +NOT_FOUND_CODE = 404 +METHOD_NOT_ALLOWED_CODE = 405 + + +def render_template(path, **kwargs): + with open(path, encoding='utf-8') as f: + template = f.read() + + for key in kwargs: + template = template.replace(f'%%{key}%%', kwargs[key]) + + return template + + +def parse_multipart_form(data: bytes): + b = BytesIO(data) + separator = b.readline().strip() + + files = [] + + while True: + line = b.readline().strip().strip(b'-') + if not line: + break + + headers_raw = [] + while not line.strip() == b'': + headers_raw.append(line.strip().decode()) + line = b.readline() + + headers = {'Content-Type': headers_raw[1].split(': ')[1]} + for pair in headers_raw[0].split(': ')[1].split('; ')[1:]: + key, value = pair.split('=') + headers[key] = value[1:-1] + + data = b'' + prev_chunk = b.read(CHUNK_SIZE) + chunk = b.read(CHUNK_SIZE) + while separator not in (prev_chunk + chunk): + data += prev_chunk + prev_chunk = chunk + chunk = b.read(CHUNK_SIZE) + + if not chunk: + break + + chunk = (prev_chunk + chunk) + if chunk.startswith(separator) or chunk.endswith(separator): + with_sep = chunk.strip(separator) + else: + if separator in chunk: + with_sep, buffer = chunk.split(separator) + b = BytesIO(buffer + b.read()) + b.readline() + + else: + with_sep = chunk + + data += with_sep + headers['data'] = data + + files.append(headers) + + return files |