Source code for willpyre.structure

from typing import Any, Tuple, Union
from urllib import parse
import email
import json
from .schema import error_schema, validate_json, ErrorResponse, schema_to_json


[docs] class TypedMultiMap(dict): def __init__(self, mapping: Union[Any, None] = None): if isinstance(mapping, TypedMultiMap): dict.__init__(self, ((k, l[:]) for k, l in mapping.lists())) # type: ignore elif isinstance(mapping, dict): temp = dict() for key, value in mapping.items(): if isinstance(value, (tuple, list)): if len(value) == 0: continue value = list(value) else: value = [value] temp[key] = value dict.__init__(self, temp) else: temp = {} for key, value in mapping or (): temp.setdefault(key, []).append(value) dict.__init__(self, temp) def __iter__(self): return dict.__iter__(self) def __getitem__(self, key): """ Returns only the first item and None, if key does not exist. """ if key in self: lst = dict.__getitem__(self, key) if len(lst) > 0: return lst[0] else: return lst return None def __setitem__(self, key, value): """ Used for assigning the value to an index. .. code-block :: python a = TypedMultiMap() a["key"] = "value" """ dict.__setitem__(self, key, [value])
[docs] def add(self, key, value): """ Inserts a key for the value given. """ dict.setdefault(self, key, []).append(value) # type: ignore
[docs] def to_dict(self, flat=True): """ Return the contents as regular dict. Args: Flat: If set to ``True``, only first item is present. Else, a list is present.\ Defaults to ``False`` """ if flat: return dict(self.items()) return dict(self.lists()) # type: ignore
[docs] def get_all(self, key, type_: Any = None): """ Fetches the list of all the items present. """ try: rv = dict.__getitem__(self, key) except KeyError: return [] if type is None: return list(rv) result = [] for item in rv: try: result.append(type_(item)) except ValueError: pass return result
[docs] def get(self, key, default=None, type_=None): if key in self: lst = dict.__getitem__(self, key) if len(lst) > 0: rv = lst[0] else: rv = lst if type_ is not None: try: rv = type_(rv) except ValueError: return rv return rv return default
[docs] def items(self, multi=False): """ Args: multi: When set to ``True``, you get a list. Else, a value. """ for key, values in dict.items(self): if multi: for value in values: yield key, value else: yield key, values[0]
[docs] def parse_multipart( content_type: str, data: bytes, decode: bool = False ) -> Tuple[TypedMultiMap, TypedMultiMap]: # pragma: no cover post_data = f"""Content-Type: {content_type} {data.decode()}""" msg = email.message_from_string(post_data) files = TypedMultiMap({}) body = TypedMultiMap({}) if msg.is_multipart(): for part in msg.get_payload(): name = part.get_param("name", header="content-disposition") filename = part.get_param("filename", header="content-disposition") payload = part.get_payload(decode=True) if filename is not None: files[name] = FileObject( {"name": name, "content": payload.decode(), "filename": filename} ) else: body[name] = payload return body, files
[docs] class Response: """ This class contains the Response data to be sent, in a manageable format. The `response` argument of the functions defined, objects of this class. The response object does not require external parameters, but has some attributes which can be set: Args: headers(dict[str,str]):\ It is the HTTP headers set as a dict. Only [content-type] = text/html is set by default. cookies(dict[str,maglev.Structure.Cookie]) body(str) status(int) """ def __init__( self, status=200, content_type="text/html", body="", headers=TypedMultiMap({}), cookies=dict(), ): self.headers = headers self.cookies = cookies self.content_type = content_type self.headers["content-type"] = self.content_type self.body = body self.status = status self.get_body = lambda : self.body
[docs] class Request: """ This class contains the information requested by the user. The functions called by ``maglev.Router.handle`` take this as the first argument. Args: headers(list[list[bytes,bytes]]): Array of headers passed by the server, and converts them to a dict. method(str): It is the HTTP request method. path(str): It is the HTTP request path. query(dict[str,list[str]]): It is obtained from the server as a string and is then parsed into the dictionary with `urllib.parse.parse_qs` """ def __init__( self, method: str, path: str, raw_body: bytes, raw_query: bytes, headers, *args, ): """ Args: self: The class ``maglev.structure.Request`` method(str): The HTTP method used by the client. path(str): The path requested by the client. headers(list): The HTTP headers obtained from the ASGI scope, of send. query(str): The ``GET`` query string, obtained from ASGI scope of send. body(str): The HTTP request body. """ self.params, self.cookies = {}, {} self.headers = TypedMultiMap({}) self.method = method self.path = path self.raw_query = raw_query self.raw_body = raw_body self.query = TypedMultiMap(parse.parse_qs(raw_query.decode())) for header_pair in headers: self.headers[header_pair[0].decode()] = header_pair[1].decode() # print("head", self.headers) content_type = self.headers.get("content-type", default="") self.content_type: str = content_type if content_type.startswith("multipart/form-data"): self.body, self.files = parse_multipart(content_type, self.raw_body) # print(self.files) elif content_type.startswith("application/json"): self.body = TypedMultiMap(json.loads(self.raw_body.decode())) self.files = TypedMultiMap({}) else: self.body = TypedMultiMap(parse.parse_qs(raw_body.decode())) self.files = TypedMultiMap({}) if "cookie" in self.headers.keys(): [ self.cookies.update({_.split("=")[0]: _.split("=")[1]}) for _ in self.headers["cookie"].split(";") # type: ignore ]
[docs] class Response404(Response): def __init__(self): super().__init__() self.headers["content-type"] = "text/html" self.body = "Not found" self.status = 404
[docs] class Response405(Response): def __init__(self): super().__init__() self.headers["content-type"] = "text/html" self.body = "Method not allowed" self.status = 405
[docs] class Response500(Response): def __init__(self): super().__init__() self.headers["content-type"] = "text/html" self.body = "Internal Server Error" self.status = 500
[docs] class Response404JSON(Response): def __init__(self): super().__init__() self.headers["content-type"] = "text/json" self.body = str( schema_to_json(error_schema("Not found", 404)), ) self.status = 404
[docs] class Response405JSON(Response): def __init__(self): super().__init__() self.headers["content-type"] = "text/json" self.body = str( schema_to_json(error_schema("Method is invalid", 405)), ) self.status = 405
[docs] class Response500JSON(Response): def __init__(self): super().__init__() self.headers["content-type"] = "text/json" self.body = str(schema_to_json(error_schema("Internal server error", 500))) self.status = 500
[docs] class Response422JSON(Response): def __init__(self): super().__init__() self.headers["content-type"] = "text/json" self.body = str(schema_to_json(error_schema("Validation error", 422))) self.status = 422
[docs] class HijackedMiddlewareResponse: def __init__(self, response: Response): self.response = response self.hijacked = True
[docs] class Redirect(Response): """ Sends a redirect response to the user. Args: - location(str): Path to send the user after redirect. - status(int): The HTTP status during redirect. Defaults to 303. (More about redirects)[https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections] """ def __init__(self, location: str, status: int = 303): super().__init__() self.body = "Redirecting to " + location self.status = ( status # https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections ) self.headers["location"] = location
[docs] class FileObject: __slots__ = ("content", "name", "filename") def __init__(self, args): self.content = args["content"] self.name = args["name"] self.filename = args["filename"] del args
[docs] class HTTPException(Exception, Response): def __init__( self, status: int = 404, body: str = "Not found", content_type="text/html" ): # Do not use super() here. Makes a mess of multiple inheritances. Response.__init__(self) self.status = 404 self.content_type = content_type self.body = body
[docs] class JSONResponse(Response): def __init__( self, data={}, status=200, content_type="application/json", headers=TypedMultiMap({}), cookies=dict(), ): super().__init__( headers=headers, cookies=cookies, content_type=content_type, status=status ) self.body = json.dumps(data)
[docs] class TextResponse(Response): def __init__( self, data="", status=200, content_type="text/plain", headers=TypedMultiMap({}), cookies=dict(), ): super().__init__( headers=headers, cookies=cookies, content_type=content_type, status=status, ) self.body = data
[docs] class HTMLResponse(Response): def __init__( self, data="", status=200, content_type="text/html", headers=TypedMultiMap({}), cookies=dict(), ): super().__init__( headers=headers, cookies=cookies, content_type=content_type, status=status, ) self.body = data