From 15cb17f26608331729846bae2b24df3850b31316 Mon Sep 17 00:00:00 2001 From: Animenosekai <40539549+Animenosekai@users.noreply.github.com> Date: Tue, 21 Feb 2023 19:27:17 +0100 Subject: [PATCH] [add] adding lots of in code docs --- LICENSE | 2 +- nasse/__main__.py | 2 +- nasse/config.py | 8 ++ nasse/docs/__init__.py | 7 +- nasse/docs/curl.py | 84 ++++++++++-- nasse/docs/example.py | 56 +++++++- nasse/docs/header.py | 141 ++++---------------- nasse/docs/javascript.py | 66 ++++++++-- nasse/docs/localization/base.py | 5 + nasse/docs/localization/eng.py | 10 ++ nasse/docs/localization/fra.py | 12 ++ nasse/docs/localization/jpn.py | 14 +- nasse/docs/markdown.py | 203 ++++++++++++++++++++++------- nasse/docs/postman.py | 39 +++++- nasse/docs/python.py | 68 ++++++++-- nasse/exceptions/__init__.py | 8 +- nasse/exceptions/arguments.py | 9 +- nasse/exceptions/authentication.py | 17 ++- nasse/exceptions/base.py | 19 ++- nasse/exceptions/request.py | 36 ++--- nasse/exceptions/validate.py | 29 ++--- nasse/nasse.py | 34 +++-- nasse/receive.py | 12 +- nasse/request.py | 4 + nasse/response.py | 13 +- nasse/servers/__init__.py | 37 ++++++ nasse/servers/flask.py | 11 +- nasse/servers/gunicorn.py | 13 ++ nasse/utils/__init__.py | 3 + nasse/utils/abstract.py | 4 + nasse/utils/annotations.py | 3 + nasse/utils/args.py | 5 +- nasse/utils/boolean.py | 3 + nasse/utils/formatter.py | 8 ++ nasse/utils/ip.py | 3 + nasse/utils/json.py | 42 +++--- nasse/utils/logging.py | 9 ++ nasse/utils/sanitize.py | 4 + nasse/utils/static.py | 1 + nasse/utils/timer.py | 13 +- 40 files changed, 751 insertions(+), 306 deletions(-) diff --git a/LICENSE b/LICENSE index f8cda7e..792f7b7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Animenosekai +Copyright (c) 2023 Animenosekai Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/nasse/__main__.py b/nasse/__main__.py index 503c49c..d2fae9d 100644 --- a/nasse/__main__.py +++ b/nasse/__main__.py @@ -1,7 +1,7 @@ """ nasse/__main__ -The CLI script +The CLI entry """ import argparse diff --git a/nasse/config.py b/nasse/config.py index 0dffda7..4c1d6f7 100644 --- a/nasse/config.py +++ b/nasse/config.py @@ -1,3 +1,7 @@ +""" +Nasse configuration +""" + import dataclasses import pathlib import typing @@ -13,6 +17,10 @@ def _alphabetic(string): @dataclasses.dataclass class NasseConfig: + """ + A configuration object for a Nasse app + """ + def verify_logger(self): from nasse.utils.logging import Logger if self.logger is None: diff --git a/nasse/docs/__init__.py b/nasse/docs/__init__.py index f023902..6f83ae9 100644 --- a/nasse/docs/__init__.py +++ b/nasse/docs/__init__.py @@ -1 +1,6 @@ -from nasse.docs import curl, example, header, javascript, markdown, postman, python \ No newline at end of file +""" +Automatic Documentation Generation on Nasse +""" + +from nasse.docs import (curl, example, header, javascript, markdown, postman, + python) diff --git a/nasse/docs/curl.py b/nasse/docs/curl.py index 137fdab..5f2768d 100644 --- a/nasse/docs/curl.py +++ b/nasse/docs/curl.py @@ -1,29 +1,95 @@ +""" +Creating `curl` examples +""" + +import typing + from nasse import models -def create_curl_example_for_method(endpoint: models.Endpoint, method: str): +def create_curl_example_for_method(endpoint: models.Endpoint, method: str) -> str: + """ + Creates a `curl` example for the given method + + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to create the example from + method: str + The method to create the example for + + Returns + ------- + str + A usage example for `curl` + """ + + def sanitize_backslashes(element: str): + """ + Turns the backslashes into triple backslashes + + Parameters + --------- + element: str + The element to sanitize + + Returns + ------- + str + The sanitized element + """ + return str(element).replace("\"", "\\\"") + params = {param.name: param.description or param.name for param in endpoint.params if param.required and ( param.all_methods or method in param.methods)} headers = {header.name: header.description or header.name for header in endpoint.headers if header.required and ( header.all_methods or method in header.methods)} + params_render = "" headers_render = "" + if len(params) > 0: - params_render = "\\\n --data-urlencode " + "\\\n --data-urlencode ".join(['"' + param.replace( - "\"", "\\\"") + '=<' + description.replace("\"", "\\\"") + '>"' for param, description in params.items()]) + " " + # ref: https://everything.curl.dev/http/post/url-encode + params_render = "\\\n --data-urlencode " + "\\\n --data-urlencode ".join( + ['"' + sanitize_backslashes(param) + + '=<' + sanitize_backslashes(description) + + '>"' for param, description in params.items()] + ) + " " if len(headers) <= 0: params_render += "\\\n " + if len(headers) > 0: - headers_render = "\\\n -H " + '\\\n -H '.join(['"' + header.replace( - "\"", "\\\"") + ': ' + description.replace("\"", "\\\"") + '"' for header, description in headers.items()]) + " " + # ref: https://everything.curl.dev/http/requests/headers + headers_render = "\\\n -H " + '\\\n -H '.join([ + '"' + sanitize_backslashes(header) + + ': ' + sanitize_backslashes(description) + + '"' for header, description in headers.items()] + ) + " " headers_render += "\\\n " + return '''curl -X {method} {params}{headers}"{path}"'''.format( - method=method, params=params_render, headers=headers_render, path=endpoint.path) + method=method, + params=params_render, + headers=headers_render, + path=endpoint.path + ) + + +def create_curl_example(endpoint: models.Endpoint) -> typing.Dict[str, str]: + """ + Creates a `curl` example command to use the endpoint. + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to create the example for -def create_curl_example(endpoint: models.Endpoint): + Returns + ------- + dict[str, str] + A dictionary of {method: example} values. + """ results = {} for method in endpoint.methods: - results[method] = create_curl_example_for_method( - endpoint=endpoint, method=method) + results[method] = create_curl_example_for_method(endpoint=endpoint, method=method) return results diff --git a/nasse/docs/example.py b/nasse/docs/example.py index e343a54..3943a8c 100644 --- a/nasse/docs/example.py +++ b/nasse/docs/example.py @@ -1,9 +1,28 @@ +""" +Generates response examples +""" + from nasse import models, utils -def _get_type(data): - """Retrieves the type of the given returning item""" +def _get_type(data: models.Return) -> str: + """ + An internal function to retrieves the type of the given returning item + + Parameters + ---------- + data: models.Return + The return value to get the type from + + Returns + ------- + "string" | "int" | "float | "bool" | "array" | "object" + The type, when possible + str + When impossible, returns the type as a string + """ key_type = data.type + if key_type is None: example = data.example if example is None: @@ -15,9 +34,13 @@ def _get_type(data): elif isinstance(example, dict): return "object" else: - return example.__class__.__name__ + try: + return example.__class__.__name__ # should be `str` + except AttributeError: + pass + key_type = str(key_type) - key_type_token = key_type.lower() + key_type_token = key_type.lower().replace(" ", "") if key_type_token in {"str", "string", "text"}: return "string" elif key_type_token in {"int", "integer"}: @@ -33,8 +56,29 @@ def _get_type(data): return key_type -def generate_example(endpoint: models.Endpoint, method: str): +def generate_example(endpoint: models.Endpoint, method: str) -> str: + """ + Generates a JSON response example + + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to get the response example from + method: str + The method to get the response example from + + Returns + ------- + str + The response example + """ def get_value(data): + """ + + Parameters + ---------- + data + """ key_type = _get_type(data) # if self.returning[key].get("nullable", False) and key_type != "object": # return None @@ -57,6 +101,8 @@ def get_value(data): if element.all_methods or method in element.methods: _response_format[element.name] = get_value(element) + # we can't consider using f-strings because they came with py3.6 + # pylint: disable=consider-using-f-string return '''{{ "success": true, "message": "Successfully processed your request", diff --git a/nasse/docs/header.py b/nasse/docs/header.py index efb2301..3c5ad20 100644 --- a/nasse/docs/header.py +++ b/nasse/docs/header.py @@ -1,13 +1,31 @@ +""" +Manages GitHub flavored markdown headers +""" + import typing -def header_link(header: str, registry: typing.List[str] = None): +def header_link(header: str, registry: typing.Optional[typing.List[str]] = None) -> str: """ - - All text is converted to lowercase. - - All non-word text (e.g., punctuation, HTML) is removed. - - All spaces are converted to hyphens. - - Two or more hyphens in a row are converted to one. - - If a header with the same ID has already been generated, a unique incrementing number is appended, starting at 1. + Generates a link from the given header + + Note: All text is converted to lowercase. + Note: All non-word text (e.g., punctuation, HTML) is removed. + Note: All spaces are converted to hyphens. + Note: Two or more hyphens in a row are converted to one. + Note: If a header with the same ID has already been generated, a unique incrementing number is appended, starting at 1. + + Parameters + ---------- + header: str + The header to generate the link from + registry: list[str], optional + An internal registry of the different created links + + Returns + ------- + str + A link to the header """ if registry is None: registry = [] @@ -29,114 +47,3 @@ def header_link(header: str, registry: typing.List[str] = None): return "{result}-{count}".format(result=final_result, count=link_count) else: return final_result - - -GETTING_STARTED_HEADER = ''' -# {name} API Reference - -Welcome to the {name} API Reference. - -## Globals - -### Response Format - -Globally, JSON responses should be formatted as follows (even when critical errors occur) - -```json -{{ - "success": true, - "message": "We successfully did this!", - "error": null, - "data": {{}} -}} -``` - -| Field | Description | Nullable | -| ------------ | ------------------------------------------------ | ---------------- | -| `success` | Wether the request was a success or not | False | -| `message` | A message describing what happened | True | -| `error` | The exception name if an error occured | True | -| `data` | The extra data, information asked in the request | False | - -### Errors - -Multiple Errors can occur, server side or request side. - -Specific errors are documented in each endpoint but these are the general errors that can occur on any endpoint: - -| Exception | Description | Code | -| --------------------------- | --------------------------------------------------------------------------------------------------------------- | ----- | -| `SERVER_ERROR` | When an error occurs on {name} while processing a request | 500 | -| `MISSING_CONTEXT` | When you are trying to access something which is only available in a Nasse context, and you aren't in one | 500 | -| `INTERNAL_SERVER_ERROR` | When a critical error occurs on the system | 500 | -| `METHOD_NOT_ALLOWED` | When you made a request with the wrong method | 405 | -| `CLIENT_ERROR` | When something is missing or is wrong with the request | 400 | -| `MISSING_VALUE` | When a value is missing from the request | 400 | -| `MISSING_PARAM` | When a parameter is missing from the request | 400 | -| `MISSING_DYNAMIC` | When a dynamic routing value is missing from the requested URL | 400 | -| `MISSING_HEADER` | When a header is missing from the request | 400 | -| `MISSING_COOKIE` | When a cookie is missing from the request | 400 | -| `AUTH_ERROR` | When an error occured while authenticating the request | 403 | - -### Authenticated Requests - -When a user needs to be logged in, the "Authorization" header needs to be set to the login token provided when logging in. - -Alternatively, the "{id}_token" parameter and "__{id}_token" cookie can be used but these won't be prioritized. - -If the endpoint is flagged for a "verified only" login, the account won't be fetched from any database but the token will be checked. - -### Debug Mode - -On debug mode (`-d` or `--debug`), multiple information are passed in the `debug` section of the response and the `DEBUG` log level is selected on the server. - -The 'debug' section is available on every type of error, except the ones issued by Flask such as `INTERNAL_SERVER_ERROR`, `METHOD_NOT_ALLOWED`, etc. (they need to do the bare minimum to not raise an exception and therefore breaking the server) - -The "call_stack" attribute is enabled only when an error occurs or the `call_stack` parameter is passed with the request. - -```json -{{ - "success": true, - "message": "We couldn't fullfil your request", - "error": null, - "data": {{ - "username": "Animenosekai" - }}, - "debug": {{ - "time": {{ - "global": 0.036757, - "verification": 0.033558, - "authentication": 0.003031, - "processing": 4.9e-05, - "formatting": 0.0001 - }}, - "ip": "127.0.0.1", - "headers": {{ - "Host": "api.{id}.com", - "Connection": "close", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language": "fr-fr", - "Accept-Encoding": "gzip, deflate, br", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15" - }}, - "values": {{}}, - "domain": "api.{id}.com", - "logs": [ - "1636562693.036563|[INFO] [nasse.receive.Receive.__call__] → Incoming GET request to /account/name from 127.0.0.1", - "1636562693.070008|[ERROR] [nasse.exceptions.base.MissingToken.__init__] An authentication token is missing from the request" - ], - "call_stack": [ - "pass the 'call_stack' parameter to get the call stack" - ] - }} -}} -``` - -''' - - -SECTION_HEADER = ''' -# {name} Section API Reference - -This file lists and explains the different endpoints available in the {name} section. -''' diff --git a/nasse/docs/javascript.py b/nasse/docs/javascript.py index d75f3cf..f510824 100644 --- a/nasse/docs/javascript.py +++ b/nasse/docs/javascript.py @@ -1,23 +1,54 @@ +""" +Generates JavaScript examples +""" + +import typing import urllib.parse from nasse import models, utils -def create_javascript_example_for_method(endpoint: models.Endpoint, method: str): - params = [param for param in endpoint.params if param.required and ( - param.all_methods or method in param.methods)] - headers = {header.name: header.description or header.name for header in endpoint.headers if header.required and ( - header.all_methods or method in header.methods)} - include_cookies = len( - [cookie for cookie in endpoint.cookies if cookie.all_methods or method in cookie.methods]) > 0 +def create_javascript_example_for_method(endpoint: models.Endpoint, method: str) -> str: + """ + Creates a JavaScript example for the given method + + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to create the example from + method: str + The method to get the example for + + Returns + ------- + str + A JavaScript example on how to use the endpoint for the given method + """ + params = [param for param in endpoint.params + if param.required and (param.all_methods or method in param.methods)] + + headers = {header.name: (header.description or header.name) + for header in endpoint.headers + if header.required and (header.all_methods or method in header.methods)} + + include_cookies = len([cookie for cookie in endpoint.cookies + if cookie.all_methods or method in cookie.methods]) > 0 + url = '"{path}"'.format(path=endpoint.path) + if len(params) > 0: - url = """`{path}?{params}`""".format(path=endpoint.path, params="&".join( - ['{escaped}=${{encodeURIComponent("{name}")}}'.format(escaped=urllib.parse.quote(param.name), name=param.name) for param in params])) + url = """`{path}?{params}`""".format( + path=endpoint.path, + params="&".join([ + '{escaped}=${{encodeURIComponent("{name}")}}'.format( + escaped=urllib.parse.quote(param.name), + name=param.name) + for param in params])) + headers_render = "" if len(headers) > 0: - headers_render = ",\n headers: {render}".format( - render=utils.json.encoder.encode(headers).replace("\n", "\n ")) + headers_render = ",\n headers: {}".format(utils.json.encoder.encode(headers).replace("\n", "\n ")) + return '''fetch({url}, {{ method: "{method}"{headers}{cookies} }}) @@ -32,7 +63,18 @@ def create_javascript_example_for_method(endpoint: models.Endpoint, method: str) }})'''.format(url=url, method=method, headers=headers_render, cookies=",\n credentials: 'include'" if include_cookies else "", path=endpoint.path) -def create_javascript_example(endpoint: models.Endpoint): +def create_javascript_example(endpoint: models.Endpoint) -> typing.Dict[str, str]: + """ + Creates JavaScript examples for the given endpoint + + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to get the example for + + Returns + ------- + """ results = {} for method in endpoint.methods: results[method] = create_javascript_example_for_method( diff --git a/nasse/docs/localization/base.py b/nasse/docs/localization/base.py index 90a6d70..ace8e13 100644 --- a/nasse/docs/localization/base.py +++ b/nasse/docs/localization/base.py @@ -1,3 +1,8 @@ +""" +The base strings +""" + + class Localization: """ Represents a Nasse documentation generation localization diff --git a/nasse/docs/localization/eng.py b/nasse/docs/localization/eng.py index 92f0839..3cc429c 100644 --- a/nasse/docs/localization/eng.py +++ b/nasse/docs/localization/eng.py @@ -1,5 +1,15 @@ +""" +The english translation + +Copyright +--------- +Animenosekai + Original Author, MIT License +""" + from nasse.docs.localization.base import Localization + class EnglishLocalization(Localization): # it's the same one as the base pass diff --git a/nasse/docs/localization/fra.py b/nasse/docs/localization/fra.py index efc5e48..3257ab6 100644 --- a/nasse/docs/localization/fra.py +++ b/nasse/docs/localization/fra.py @@ -1,7 +1,19 @@ +""" +The french translation + +Copyright +--------- +Animenosekai + Original Author, MIT License +""" + from nasse.docs.localization.base import Localization class FrenchLocalization(Localization): + """ + The french translation for the docs generation + """ sections = "Sections" getting_started = "Pour commencer" diff --git a/nasse/docs/localization/jpn.py b/nasse/docs/localization/jpn.py index 7997245..a0b3059 100644 --- a/nasse/docs/localization/jpn.py +++ b/nasse/docs/localization/jpn.py @@ -1,7 +1,19 @@ +""" +The japanese translation + +Copyright +--------- +Animenosekai + Original Author, MIT License +""" + from nasse.docs.localization.base import Localization class JapaneseLocalization(Localization): + """ + The japanese version of the docs generation + """ sections = "セクション" getting_started = "はじめに" @@ -36,7 +48,7 @@ class JapaneseLocalization(Localization): example = "例" # Response - response = "レスポンス" # 応答や解答より「レスポンス」の方が使われているよう + response = "レスポンス" # 応答や解答より「レスポンス」の方が使われているよう example_response = "レスポンスの例" not_json_response = "このエンドポイントは、JSON形式のレスポンスを返さないようです。" diff --git a/nasse/docs/markdown.py b/nasse/docs/markdown.py index 03093e7..6b9224c 100644 --- a/nasse/docs/markdown.py +++ b/nasse/docs/markdown.py @@ -1,3 +1,15 @@ +""" +Automatically generates Markdown Docs + +Copyright +--------- +Animenosekai + Original author, MIT License +""" + +# pylint: disable=consider-using-f-string + +import typing import urllib.parse from pathlib import Path @@ -7,71 +19,151 @@ from nasse.utils.sanitize import sort_http_methods -def make_docs(endpoint: models.Endpoint, postman: bool = False, curl: bool = True, javascript: bool = True, python: bool = True, localization: Localization = Localization()): - result = ''' +def make_docs(endpoint: models.Endpoint, + postman: bool = False, + curl: bool = True, + javascript: bool = True, + python: bool = True, + localization: Localization = Localization()) -> str: + """ + Generates the documentation for the given endpoint + + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to generate the docs for + postman: bool, default = False + If the docs are generated for Postman + (doesn't include all of the details since a lot is already included by the Postamn docs generator) + curl: bool, default = True + If the `curl` examples should be included too + javascript: bool, default = True + If the JavaScript examples should be included too + python: bool, default = True + If the Python examples should be included too + localization: Localization, optional + The language to use to create the docs + + Returns + ------- + str + The generated docs + """ + + result = """ # {name} -'''.format(name=endpoint.name) - if len(endpoint.methods) == 1: - result += ''' -{description} -'''.format(description=endpoint.description.get(endpoint.methods[0] if "*" not in endpoint.description else "*", localization.no_description)) - result += make_docs_for_method(endpoint=endpoint, postman=postman, curl=curl, javascript=javascript, python=python, localization=localization) +""".format(name=endpoint.name) + + kwargs = {"endpoint": endpoint, + "postman": postman, + "curl": curl, + "javascript": javascript, + "python": python, + "localization": localization} + + if len(endpoint.methods) <= 1: + result += """ +{} +""".format(endpoint.description.get(endpoint.methods[0] if "*" not in endpoint.description else "*", + localization.no_description)) + result += make_docs_for_method(**kwargs) else: for method in sort_http_methods(endpoint.methods): - result += "\n - ### {localization__using_method}".format(localization__using_method=localization.using_method.format(method=method)) - result += "\n{docs}\n".format(docs=make_docs_for_method(endpoint=endpoint, method=method, - postman=postman, curl=curl, javascript=javascript, python=python, localization=localization)) + kwargs["method"] = method + result += "\n - ### {}".format(localization.using_method.format(method=method)) + result += "\n{}\n".format(make_docs_for_method(**kwargs)) return result -def make_docs_for_method(endpoint: models.Endpoint, method: str = None, postman: bool = False, curl: bool = True, javascript: bool = True, python: bool = True, localization: Localization = Localization()): - result = '' - -# ENDPOINT HEADER +def make_docs_for_method( + endpoint: models.Endpoint, + method: typing.Optional[str] = None, + postman: bool = False, + curl: bool = True, + javascript: bool = True, + python: bool = True, + localization: Localization = Localization()) -> str: + """ + Creates the docs for the given method + + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to generate the docs for + method: str, optional + The method to generate the docs for + postman: bool, default = False + If the docs are generated for Postman + (doesn't include all of the details since a lot is already included by the Postamn docs generator) + curl: bool, default = True + If the `curl` examples should be included too + javascript: bool, default = True + If the JavaScript examples should be included too + python: bool, default = True + If the Python examples should be included too + localization: Localization, optional + The language to use to create the docs + + Returns + ------- + str + The generated docs + """ + result = "" if len(endpoint.methods) == 1 or method is None: + # treating it as only a single method endpoint heading_level = "###" method = list(endpoint.methods)[0] else: heading_level = "####" method = str(method) - result += ''' -{description} -'''.format(description=endpoint.description.get(method if method in endpoint.description else "*", localization.no_description)) + result += """ +{} +""".format(endpoint.description.get(method if method in endpoint.description else "*", + localization.no_description)) try: path = Path(endpoint.handler.__code__.co_filename).resolve().relative_to(Path().resolve()) except Exception: path = Path(endpoint.handler.__code__.co_filename) - line = endpoint.handler.__code__.co_firstlineno + line = endpoint.handler.__code__.co_firstlineno -# ENDPOINT HEADING + # ENDPOINT HEADING if not postman: - result += ''' + result += """ ```http {method} {path} ``` > [{source_code_path}]({github_path}) -'''.format(method=method, path=endpoint.path, source_code_path=path, github_path="../../{path}#L{line}".format(path=path, line=line)) +""".format(method=method, + path=endpoint.path, + source_code_path=path, + # FIXME: this needs to be fixed because it sometimes fails + github_path="../../{path}#L{line}".format(path=path, line=line)) else: - result = ''' + result = """ > [{source_code_path}]({github_path}) {description} -'''.format(source_code_path=path, github_path="../../{path}#L{line}".format(path=path, line=line), description=endpoint.description.get(method if method in endpoint.description else "*", localization.no_description)) +""".format(source_code_path=path, + github_path="../../{path}#L{line}".format(path=path, line=line), + description=endpoint.description.get(method if method in endpoint.description else "*", + localization.no_description)) # AUTHENTICATION - result += ''' + result += """ {heading} {localization__authentication} -'''.format(heading=heading_level, localization__authentication=localization.authentication) +""".format(heading=heading_level, localization__authentication=localization.authentication) + login_rules = endpoint.login.get(method, endpoint.login.get("*", None)) if login_rules is None: result += localization.no_auth_rule @@ -105,13 +197,13 @@ def make_docs_for_method(endpoint: models.Endpoint, method: str = None, postman: ]: params = [param for param in values if (param.all_methods or method in param.methods)] if len(params) > 0: - result += ''' + result += """ {heading} {field} | {localization__name} | {localization__description} | {localization__required} | {localization__type} | | ------------ | -------------------------------- | ---------------- | ---------------- | -'''.format(field=field, heading=heading_level, localization__name=localization.name, localization__description=localization.description, localization__required=localization.required, localization__type=localization.type) +""".format(field=field, heading=heading_level, localization__name=localization.name, localization__description=localization.description, localization__required=localization.required, localization__type=localization.type) result += "\n".join( ["| `{param}` | {description} | {required} | {type} |".format(param=param.name, description=param.description, required=localization.yes if param.required else localization.no, type=param.type.__name__ if hasattr(param.type, "__name__") else str(param.type) if param.type is not None else "str") for param in params]) @@ -119,19 +211,19 @@ def make_docs_for_method(endpoint: models.Endpoint, method: str = None, postman: # LANGUAGE SPECIFIC EXAMPLES if any((curl, javascript, python)): - result += ''' + result += """ {heading} {localization__example} -'''.format(heading=heading_level, localization__example=localization.example) +""".format(heading=heading_level, localization__example=localization.example) for proceed, highlight, language, function in [ (curl, "bash", "cURL", docs.curl.create_curl_example_for_method), (javascript, "javascript", "JavaScript", docs.javascript.create_javascript_example_for_method), (python, "python", "Python", docs.python.create_python_example_for_method) ]: if proceed: - result += ''' + result += """
{language} {localization__example} @@ -143,30 +235,36 @@ def make_docs_for_method(endpoint: models.Endpoint, method: str = None, postman: ```
-'''.format(heading=heading_level + "#", localization__example=localization.example, highlight=highlight, language=language, example=function(endpoint, method=method)) - result += '''''' +""".format(heading=heading_level + "#", + localization__example=localization.example, + highlight=highlight, + language=language, + example=function(endpoint, method=method)) + result += """""" # RESPONSE returning = [element for element in endpoint.returning if (element.all_methods or method in element.methods)] if len(returning) > 0: - result += ''' + result += """ {heading} {localization__response} -'''.format(heading=heading_level, localization__response=localization.response) +""".format(heading=heading_level, localization__response=localization.response) # JSON RESPONSE EXAMPLE if endpoint.json: - result += ''' + result += """ {heading} {localization__example_response} ```json {example} ``` -'''.format(heading=heading_level + "#", localization__example_response=localization.example_response, example=docs.example.generate_example(endpoint, method=method)) +""".format(heading=heading_level + "#", + localization__example_response=localization.example_response, + example=docs.example.generate_example(endpoint, method=method)) else: result += "\n" result += localization.not_json_response @@ -174,14 +272,23 @@ def make_docs_for_method(endpoint: models.Endpoint, method: str = None, postman: # RESPONSE DESCRIPTION - result += ''' + result += """ {heading} {localization__returns} | {localization__field} | {localization__description} | {localization__type} | {localization__nullable} | | ---------- | -------------------------------- | ------ | --------- | -'''.format(heading=heading_level + "#", localization__returns=localization.returns, localization__field=localization.field, localization__description=localization.description, localization__type=localization.type, localization__nullable=localization.nullable) +""".format(heading=heading_level + "#", + localization__returns=localization.returns, + localization__field=localization.field, + localization__description=localization.description, + localization__type=localization.type, + localization__nullable=localization.nullable) + result += "\n".join(["| `{key}` | {description} | {type} | {nullable} |".format(key=element.name, - description=element.description, type=docs.example._get_type(element), nullable=localization.yes if element.nullable else localization.no) for element in returning]) + description=element.description, + type=docs.example._get_type(element), + nullable=localization.yes if element.nullable else localization.no) + for element in returning]) result += "\n" @@ -191,15 +298,21 @@ def make_docs_for_method(endpoint: models.Endpoint, method: str = None, postman: errors = [error for error in endpoint.errors if ( error.all_methods or method in error.methods)] if len(errors) > 0: - result += ''' + result += """ {heading} {localization__possible_errors} | {localization__exception} | {localization__description} | {localization__code} | | --------------- | -------------------------------- | ------ | -'''.format(heading=heading_level + "#", localization__possible_errors=localization.possible_errors, localization__exception=localization.exception, localization__description=localization.description, localization__code=localization.code) - - result += "\n".join( - ["| `{key}` | {description} | {code} |".format(key=error.name, description=error.description, code=error.code) for error in errors]) +""".format(heading=heading_level + "#", + localization__possible_errors=localization.possible_errors, + localization__exception=localization.exception, + localization__description=localization.description, + localization__code=localization.code) + + result += "\n".join(["| `{key}` | {description} | {code} |".format(key=error.name, + description=error.description, + code=error.code) + for error in errors]) # INDEX LINKING diff --git a/nasse/docs/postman.py b/nasse/docs/postman.py index 7dd05da..da0e410 100644 --- a/nasse/docs/postman.py +++ b/nasse/docs/postman.py @@ -1,3 +1,7 @@ +""" +Generates docs for Postman +""" + import typing from copy import deepcopy # from uuid import uuid4 @@ -7,7 +11,25 @@ from nasse.utils.sanitize import sort_http_methods -def create_postman_data(app, section: str, endpoints: typing.List[models.Endpoint], localization: Localization = Localization()): +def create_postman_data(app, section: str, endpoints: typing.List[models.Endpoint], localization: Localization = Localization()) -> typing.Dict[str, typing.Any]: + """ + Generates the data for postman + + Parameters + ---------- + app + section: str + The section to document + endpoint: list[models.Endpoint] + A list of endpoints + localization: Localization + The language of the docs + + Returns + ------- + dict[str, Any] + The data for Postman + """ postman_section = { "info": { # "_postman_id": str(uuid4()), @@ -38,6 +60,21 @@ def create_postman_data(app, section: str, endpoints: typing.List[models.Endpoin def create_postman_docs(endpoint: models.Endpoint, localization: Localization = Localization()): + """ + Generates the Postman docs for an endpoint + + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to generate the docs for + localization: Localization + The language to use + + Returns + ------- + list[dict[str, Any]] + A list of documentation data for Postman (one item per method) + """ results = [] for method in sort_http_methods(endpoint.methods): result = { diff --git a/nasse/docs/python.py b/nasse/docs/python.py index d42d199..2923ea9 100644 --- a/nasse/docs/python.py +++ b/nasse/docs/python.py @@ -1,31 +1,77 @@ +""" +Generates Python usage examples +""" + import json +import typing from nasse import models -def create_python_example_for_method(endpoint: models.Endpoint, method: str): - params = {param.name: param.description or param.name for param in endpoint.params if param.required and ( - param.all_methods or method in param.methods)} - headers = {header.name: header.description or header.name for header in endpoint.headers if header.required and ( - header.all_methods or method in header.methods)} +def create_python_example_for_method(endpoint: models.Endpoint, method: str) -> str: + """ + Generates a Python example for the given endpoint + + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to create the example for + method: str + The method to create the example for + + Returns + ------- + str + The example + """ + params = {param.name: (param.description or param.name) + for param in endpoint.params + if param.required and (param.all_methods or method in param.methods)} + + headers = {header.name: (header.description or header.name) + for header in endpoint.headers + if header.required and (header.all_methods or method in header.methods)} + params_render = "" headers_render = "" + if len(params) > 0: - params_render = ",\n params = {render}".format(render=json.dumps( - params, ensure_ascii=False, indent=4).replace("\n", "\n ")) + params_render = ",\n params = {}".format(json.dumps(params, + ensure_ascii=False, + indent=4) + .replace("\n", "\n ")) + if len(headers) > 0: - headers_render = ",\n headers = {render}".format(render=json.dumps( - headers, ensure_ascii=False, indent=4).replace("\n", "\n ")) + headers_render = ",\n headers = {}".format(json.dumps(headers, + ensure_ascii=False, + indent=4) + .replace("\n", "\n ")) return '''import requests r = requests.request("{method}", "{path}"{params}{headers}) if r.status_code >= 400 or not r.json()["success"]: raise ValueError("An error occured while requesting for {path}, error: " + r.json()["error"]) print("Successfully requested for {path}") -print(r.json()["data"])'''.format(method=method, path=endpoint.path, params=params_render, headers=headers_render) +print(r.json()["data"])'''.format(method=method, + path=endpoint.path, + params=params_render, + headers=headers_render) + + +def create_python_example(endpoint: models.Endpoint) -> typing.Dict[str, str]: + """ + Generates Python examples for the given endpoint + Parameters + ---------- + endpoint: models.Endpoint + The endpoint to create the example for -def create_python_example(endpoint: models.Endpoint): + Returns + ------- + dict[str, str] + A dictionary containing {method: example} values + """ results = {} for method in endpoint.methods: results[method] = create_python_example_for_method( diff --git a/nasse/exceptions/__init__.py b/nasse/exceptions/__init__.py index 1ec76e4..9904c8a 100644 --- a/nasse/exceptions/__init__.py +++ b/nasse/exceptions/__init__.py @@ -1,9 +1,13 @@ """ File containing the different exceptions which can be raised in Nasse """ + +import typing from nasse.exceptions.base import NasseException from nasse.exceptions import arguments, authentication, request, validate + class NasseExceptionTBD(NasseException): - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) + """ + If an exception is expected to be defined later on + """ diff --git a/nasse/exceptions/arguments.py b/nasse/exceptions/arguments.py index 68c5da4..029988c 100644 --- a/nasse/exceptions/arguments.py +++ b/nasse/exceptions/arguments.py @@ -1,10 +1,13 @@ +""" +Internal Nasse error about arguments +""" + +import typing from nasse.exceptions.base import NasseException class MissingArgument(NasseException): + """When an argument is missing""" STATUS_CODE = 500 MESSAGE = "An argument is missing" EXCEPTION_NAME = "MISSING_INTERNAL_ARGUMENT" - - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) diff --git a/nasse/exceptions/authentication.py b/nasse/exceptions/authentication.py index 8857100..adb446f 100644 --- a/nasse/exceptions/authentication.py +++ b/nasse/exceptions/authentication.py @@ -1,25 +1,24 @@ +""" +Exceptions handling the authentication errors +""" + +import typing from nasse.exceptions.base import NasseException class AuthenticationError(NasseException): + """When an error occurs with the user authentication step""" MESSAGE = "An error occured while checking your authentication" EXCEPTION_NAME = "AUTH_ERROR" STATUS_CODE = 403 - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) - class MissingToken(AuthenticationError): + """When the token is missing to authenticate""" LOG = False MESSAGE = "The authentication token is missing from your request" - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) - class Forbidden(AuthenticationError): + """When the user is not allowed to access something""" MESSAGE = "You are not allowed to access this endpoint" - - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) diff --git a/nasse/exceptions/base.py b/nasse/exceptions/base.py index d6dbb1d..7294f64 100644 --- a/nasse/exceptions/base.py +++ b/nasse/exceptions/base.py @@ -1,16 +1,33 @@ +""" +Defines the base exception +""" +import typing from nasse.utils.logging import logger class NasseException(Exception): + """ + Represents an exception, providing a bit more context for the server to actually give back the best answers possible. + + Here are the different constants which can be set + + - STATUS_CODE: The actual status code to respond with, defaults to 500 + - MESSAGE: A message accompanying the error, optional + - EXCEPTION_NAME: Explicitely giving the exception name, defaults to "SERVER_ERROR" + - LOG: If the error should be logged to the console, defaults to True + """ STATUS_CODE = 500 MESSAGE = "An unexpected error occured on the server" EXCEPTION_NAME = "SERVER_ERROR" LOG = True - def __init__(self, message: str = None, *args: object) -> None: + def __init__(self, *args: object, message: typing.Optional[str] = None) -> None: if message is not None: self.MESSAGE = str(message) + super().__init__(self.MESSAGE, *args) + if self.LOG: + # should be the context's logger logger.error(self.MESSAGE) diff --git a/nasse/exceptions/request.py b/nasse/exceptions/request.py index 55fddef..40d85a7 100644 --- a/nasse/exceptions/request.py +++ b/nasse/exceptions/request.py @@ -2,66 +2,54 @@ class ClientError(NasseException): + """When there is a client side error""" STATUS_CODE = 400 MESSAGE = "Something is missing from the request" EXCEPTION_NAME = "CLIENT_ERROR" - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) - class MissingValue(ClientError): + """When a value is missing from the request""" MESSAGE = "A value is missing from your request" EXCEPTION_NAME = "MISSING_VALUE" - def __init__(self, name: str = "", missing_type: str = "value", *args: object) -> None: - message = "'{name}' is a required request {type}".format( - name=name, type=str(missing_type)) + def __init__(self, *args: object, name: str = "", missing_type: str = "value") -> None: + message = "'{name}' is a required request {type}".format(name=name, + type=str(missing_type)) super().__init__(message=message, *args) class MissingParam(MissingValue): + """When a parameter is missing""" MESSAGE = "A parameter is missing from your request" EXCEPTION_NAME = "MISSING_PARAM" - def __init__(self, name: str = "", missing_type: str = "parameter", *args: object) -> None: - super().__init__(name=name, missing_type=missing_type, *args) - class MissingDynamic(MissingValue): + """When a dynamic routing value is missing""" MESSAGE = "A dynamic routing value is missing from your request" EXCEPTION_NAME = "MISSING_DYNAMIC" - def __init__(self, name: str = "", missing_type: str = "parameter", *args: object) -> None: - super().__init__(name=name, missing_type=missing_type, *args) - class MissingHeader(MissingValue): + """When a header is missing""" MESSAGE = "A header is missing from your request" EXCEPTION_NAME = "MISSING_HEADER" - def __init__(self, name: str = "", missing_type: str = "header", *args: object) -> None: - super().__init__(name=name, missing_type=missing_type, *args) - class MissingCookie(MissingValue): + """When a cookie is missing""" MESSAGE = "A cookie is missing from your request" EXCEPTION_NAME = "MISSING_COOKIE" - def __init__(self, name: str = "", missing_type: str = "cookie", *args: object) -> None: - super().__init__(name=name, missing_type=missing_type, *args) - class MissingContext(NasseException): + """Server side error when a value only accessible in a Nasse context is accessed outside of oneƒ""" MESSAGE = "You are not actively in a Nasse context" EXCEPTION_NAME = "MISSING_CONTEXT" STATUS_CODE = 500 - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) - class MissingEndpoint(MissingContext): - - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) + """When an endpoint is missing""" + # ?? diff --git a/nasse/exceptions/validate.py b/nasse/exceptions/validate.py index ba3226b..ee25142 100644 --- a/nasse/exceptions/validate.py +++ b/nasse/exceptions/validate.py @@ -1,47 +1,40 @@ +""" +A list of internal exceptions for Nasse validations +""" from nasse.exceptions.base import NasseException class ValidationError(NasseException): + """When there is an input validation error""" MESSAGE = "Nasse couldn't validate an input" EXCEPTION_NAME = "VALIDATION_ERROR" - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) - class ConversionError(ValidationError): + """An internal error occuring when Nasse can't convert an input to a Nasse object""" EXCEPTION_NAME = "INTERNAL_CONVERSION_ERROR" MESSAGE = "Nasse couldn't convert a given input to a Nasse object instance" - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) - class MethodsConversionError(ConversionError): - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) + """An internal error occuring when Nasse can't convert an input to methods""" class ReturnConversionError(ConversionError): - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) + """An internal error occuring when Nasse can't convert an input to a Nasse `Return` object""" class UserSentConversionError(ConversionError): - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) + """An internal error occuring when Nasse can't convert an input to a Nasse `UserSent` object""" class ErrorConversionError(ConversionError): - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) + """An internal error occuring when Nasse can't convert an input to a Nasse `Error` object""" class LoginConversionError(ConversionError): - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) + """An internal error occuring when Nasse can't convert an input to a Nasse `Login` object""" class CookieConversionError(ConversionError): - def __init__(self, message: str = None, *args: object) -> None: - super().__init__(message=message, *args) + """An internal error occuring when Nasse can't convert an input to a Nasse `Cookie` object""" diff --git a/nasse/nasse.py b/nasse/nasse.py index 7cd3492..7ac7c14 100644 --- a/nasse/nasse.py +++ b/nasse/nasse.py @@ -124,15 +124,17 @@ def log(self, *msg, **kwargs): def route(self, path: str = utils.annotations.Default(""), - endpoint: models.Endpoint = None, - flask_options: dict = None, **kwargs): + endpoint: typing.Optional[models.Endpoint] = None, + flask_options: typing.Optional[dict] = None, **kwargs): """ # A decorator to register a new endpoint - Can be used like so: + Examples + -------- + >>> # Can be used like so: >>> from nasse import Nasse, Param >>> app = Nasse() - >>> + >>> >>> @app.route(path="/hello", params=Param(name="username", description="the person welcomed")) ... def hello(params: dict): ... if "username" in params: @@ -141,9 +143,11 @@ def route(self, Parameters ----------- - `endpoint`: nasse.models.Endpoint + path: str, default = "" + The path to register the handler to + endpoint: models.Endpoint A base endpoint object. Other given values will overwrite the values from this Endpoint object. - `flask_options`: dict + flask_options: dict If needed, extra options to give to flask.Flask `**kwargs` The same options that will be passed to nasse.models.Endpoint to create the new endpoint. \n @@ -159,7 +163,10 @@ def decorator(f): results.update(kwargs) results["handler"] = f new_endpoint = models.Endpoint(**results) - flask_options["methods"] = new_endpoint.methods if "*" not in new_endpoint.methods else utils.types.HTTPMethod.ACCEPTED + try: + flask_options["methods"] = new_endpoint.methods if "*" not in new_endpoint.methods else utils.types.HTTPMethod.ACCEPTED + except Exception: + pass self.flask.add_url_rule(new_endpoint.path, flask_options.pop( "endpoint", None), receive.Receive(self, new_endpoint), **flask_options) self.endpoints[new_endpoint.path] = new_endpoint @@ -213,10 +220,11 @@ def __overwrite__(self, *args, **kwargs): return self self.instance = server(app=self, config=self.config) with (rich.progress.Progress(*(rich.progress.TextColumn("[progress.description]{task.description}"), - rich.progress.TextColumn("—"), - rich.progress.TimeElapsedColumn()), - transient=True) if status else MockProgress()) as progress: - progress.add_task(description='🍡 {name} is running on {host}:{port}'.format(name=self.config.name, host=self.config.host, port=self.config.port)) + rich.progress.TextColumn("—"), + rich.progress.TimeElapsedColumn()), + transient=True) if status else MockProgress()) as progress: + progress.add_task(description='🍡 {name} is running on {host}:{port}'.format( + name=self.config.name, host=self.config.host, port=self.config.port)) self.config.logger.log("🎏 Press {cyan}Ctrl+C{normal} to quit") self.config.logger.log("🌍 Binding to {{magenta}}{host}:{port}{{normal}}" .format(host=self.config.host, @@ -279,12 +287,12 @@ def after_request(self, response: flask.Response): Parameters ----------- - `response`: flask.flask.Response + response: flask.flask.Response The response to send back Returns -------- - `flask.Response`: + flask.Response: The response to send """ try: diff --git a/nasse/receive.py b/nasse/receive.py index 262bd7a..0d883a6 100644 --- a/nasse/receive.py +++ b/nasse/receive.py @@ -1,3 +1,6 @@ +""" +This is where the requests are received and first processed +""" import base64 import inspect import sys @@ -18,8 +21,8 @@ def retrieve_token(context: request.Request = None) -> str: Parameters ---------- - context: nasse.request.request.Request - The current request, if not properly set, the current context is used. + context: nasse.request.request.Request + The current request, if not properly set, the current context is used. """ if not isinstance(context, request.Request): context = flask.g.request or flask.request @@ -48,8 +51,7 @@ def __init__(self, app, endpoint: models.Endpoint) -> None: self.app = app self.endpoint = endpoint RECEIVERS_COUNT += 1 - self.__name__ = "__nasse_receiver_{number}".format( - number=RECEIVERS_COUNT) + self.__name__ = "__nasse_receiver_{number}".format(number=RECEIVERS_COUNT) def __call__(self, *args: typing.Any, **kwds: typing.Any) -> typing.Any: try: @@ -398,7 +400,7 @@ def __call__(self, *args: typing.Any, **kwds: typing.Any) -> typing.Any: color = "{yellow}" else: color = "{magenta}" - if final.status_code < 200: # ? + if final.status_code < 200: # ? status_color = "{white}" elif final.status_code < 300: status_color = "{blue}" diff --git a/nasse/request.py b/nasse/request.py index 9eee932..f6d3ff6 100644 --- a/nasse/request.py +++ b/nasse/request.py @@ -1,3 +1,7 @@ +""" +This is where the request context is created and gets sanitized +""" + import typing import flask diff --git a/nasse/response.py b/nasse/response.py index 6289594..985fae3 100644 --- a/nasse/response.py +++ b/nasse/response.py @@ -1,3 +1,6 @@ +""" +This is where the responses and errors are partly processed +""" import datetime import typing @@ -8,7 +11,7 @@ from nasse.utils.annotations import Default -def exception_to_response(value: Exception, config: config.NasseConfig = None): +def exception_to_response(value: Exception, config: typing.Optional[config.NasseConfig] = None): """ Internal function to turn an exception to a tuple of values that can be used to make a response """ @@ -17,7 +20,7 @@ def exception_to_response(value: Exception, config: config.NasseConfig = None): error = value.EXCEPTION_NAME code = int(value.STATUS_CODE) elif isinstance(value, werkzeug.exceptions.HTTPException): - code = value.code + code = value.code or 500 if code >= 500: # we don't know what kind of exception it might leak data = "An error occured on the server while processing your request" # we consider that they are fewer non basic exceptions (non 500) that are dangerous to leak (i.e: 4xx errors are related to the client) @@ -56,9 +59,9 @@ def _cookie_validation(value): raise exceptions.validate.CookieConversionError( "Either 'name' is missing or one argument doesn't have the right type while creating a Nasse.response.ResponseCookie instance") raise ValueError # will be catched - except Exception as e: - if isinstance(e, exceptions.NasseException): - raise e + except Exception as err: + if isinstance(err, exceptions.NasseException): + raise err raise exceptions.validate.CookieConversionError( "Nasse cannot convert value of type <{t}> to Nasse.response.ResponseCookie".format(t=value.__class__.__name__)) diff --git a/nasse/servers/__init__.py b/nasse/servers/__init__.py index e69de29..ca1b9da 100644 --- a/nasse/servers/__init__.py +++ b/nasse/servers/__init__.py @@ -0,0 +1,37 @@ +""" +This folder contains the different HTTP WSGI server backends +""" +import abc +from nasse import config + + +class ABC(metaclass=abc.ABCMeta): + """Helper class that provides a standard way to create an ABC using + inheritance. + + Added in the ABC module in Python 3.4 + """ + __slots__ = () + + +class ServerBackend(ABC): + """ + A server backend + """ + + def __init__(self, app: "Nasse", config: config.NasseConfig) -> None: + self.app = app + self.config = config or config.NasseConfig() + + @abc.abstractmethod + def run(self, *args, **kwargs): + """ + Should contain the server running logic + """ + return None + + @abc.abstractmethod + def stop(self): + """ + Should contain the server termination logic + """ diff --git a/nasse/servers/flask.py b/nasse/servers/flask.py index 2266997..77ead85 100644 --- a/nasse/servers/flask.py +++ b/nasse/servers/flask.py @@ -1,8 +1,15 @@ +""" +This is the flask default server backend +""" import werkzeug -from nasse import config +from nasse import config, servers -class Flask: +class Flask(servers.ServerBackend): + """ + The default Flask server backend + """ + def __init__(self, app: "Nasse", config: config.NasseConfig) -> None: self.app = app self.config = config or config.NasseConfig() diff --git a/nasse/servers/gunicorn.py b/nasse/servers/gunicorn.py index 6dee82b..18cffda 100644 --- a/nasse/servers/gunicorn.py +++ b/nasse/servers/gunicorn.py @@ -1,3 +1,9 @@ +""" +The Gunicorn backend + +Note: Should be more suitable for production use +""" + import multiprocessing from nasse import Nasse, config @@ -9,7 +15,14 @@ class Gunicorn(Flask): + """ + The Gunicorn backend + """ class BaseApp(gunicorn.app.base.BaseApplication): + """ + The internal Gunicorn base app + """ + def __init__(self, app: Nasse, config: config.NasseConfig = None, **kwargs): self.app = app self.config = config or config.NasseConfig() diff --git a/nasse/utils/__init__.py b/nasse/utils/__init__.py index 74481c3..4286012 100644 --- a/nasse/utils/__init__.py +++ b/nasse/utils/__init__.py @@ -1 +1,4 @@ +""" +A set of commonly used utilities for web servers +""" from nasse.utils import abstract, annotations, args, boolean, ip, json, logging, sanitize, timer, types, xml, formatter diff --git a/nasse/utils/abstract.py b/nasse/utils/abstract.py index eb80209..66ad6cb 100644 --- a/nasse/utils/abstract.py +++ b/nasse/utils/abstract.py @@ -1,3 +1,7 @@ +""" +A minimal polyfill for more python versions +""" + import abc from abc import abstractmethod diff --git a/nasse/utils/annotations.py b/nasse/utils/annotations.py index daba448..8988b6e 100644 --- a/nasse/utils/annotations.py +++ b/nasse/utils/annotations.py @@ -1,3 +1,6 @@ +""" +Annotation utils +""" import typing diff --git a/nasse/utils/args.py b/nasse/utils/args.py index e7268ea..9f79740 100644 --- a/nasse/utils/args.py +++ b/nasse/utils/args.py @@ -1,7 +1,8 @@ +""" +Provides a lightweight and easy way to access CLI arguments +""" import sys -# from nasse import exceptions - class NoDefault: pass diff --git a/nasse/utils/boolean.py b/nasse/utils/boolean.py index 11d86fa..225def6 100644 --- a/nasse/utils/boolean.py +++ b/nasse/utils/boolean.py @@ -1,3 +1,6 @@ +""" +A utility to parse boolean values +""" import typing from nasse import utils diff --git a/nasse/utils/formatter.py b/nasse/utils/formatter.py index 6407592..a437a7a 100644 --- a/nasse/utils/formatter.py +++ b/nasse/utils/formatter.py @@ -1,3 +1,11 @@ +""" +A string formatter for Nasse + +Copyright +--------- +Animenosekai + MIT License +""" import datetime import enum import inspect diff --git a/nasse/utils/ip.py b/nasse/utils/ip.py index 21b0597..c146c61 100644 --- a/nasse/utils/ip.py +++ b/nasse/utils/ip.py @@ -1,3 +1,6 @@ +""" +A utility to retrieve the IP address of the client +""" import flask diff --git a/nasse/utils/json.py b/nasse/utils/json.py index c39c296..eb4c8ba 100644 --- a/nasse/utils/json.py +++ b/nasse/utils/json.py @@ -1,3 +1,7 @@ +""" +The Nasse JSON Encoder +""" + import base64 import io import json @@ -12,19 +16,19 @@ class NasseJSONEncoder(json.JSONEncoder): def _make_iterencode(self, markers, _default, _encoder, _indent, _floatstr, - _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, - # HACK: hand-optimized bytecode; turn globals into locals - ValueError=ValueError, - dict=dict, - float=float, - id=id, - int=int, - isinstance=isinstance, - list=list, - str=str, - tuple=tuple, - _intstr=int.__repr__, - ): + _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + # HACK: hand-optimized bytecode; turn globals into locals + ValueError=ValueError, + dict=dict, + float=float, + id=id, + int=int, + isinstance=isinstance, + list=list, + str=str, + tuple=tuple, + _intstr=int.__repr__, + ): if _indent is not None and not isinstance(_indent, str): _indent = ' ' * _indent @@ -84,7 +88,10 @@ def _iterencode_list(lst, _current_indent_level): yield '\n' + _indent * _current_indent_level yield ']' if markers is not None: - del markers[markerid] + try: + del markers[markerid] + except Exception: + pass def _iterencode_dict(dct, _current_indent_level): dct = self.default(dct) @@ -165,7 +172,10 @@ def _iterencode_dict(dct, _current_indent_level): yield '\n' + _indent * _current_indent_level yield '}' if markers is not None: - del markers[markerid] + try: + del markers[markerid] + except Exception: + pass def _iterencode(o, _current_indent_level): o = _default(o) @@ -279,4 +289,4 @@ def default(self, o: typing.Any) -> typing.Any: encoder = NasseJSONEncoder(ensure_ascii=False, indent=4) -minified_encoder = NasseJSONEncoder(ensure_ascii=False, separators=(",", ":")) \ No newline at end of file +minified_encoder = NasseJSONEncoder(ensure_ascii=False, separators=(",", ":")) diff --git a/nasse/utils/logging.py b/nasse/utils/logging.py index 9cec63c..1854d2c 100644 --- a/nasse/utils/logging.py +++ b/nasse/utils/logging.py @@ -1,3 +1,11 @@ +""" +Nasse logging utilities + +Copyright +--------- +Animenosekai + Original author, MIT License +""" import dataclasses import datetime import enum @@ -174,6 +182,7 @@ def print_exception(self, show_locals: bool = False, **kwargs): exception = print_exception + RECORDING = False CALL_STACK = [] diff --git a/nasse/utils/sanitize.py b/nasse/utils/sanitize.py index 0da807c..ea68ef9 100644 --- a/nasse/utils/sanitize.py +++ b/nasse/utils/sanitize.py @@ -1,3 +1,7 @@ +""" +Nasse's sanitizing and convert utility +""" + from ctypes import util import typing diff --git a/nasse/utils/static.py b/nasse/utils/static.py index 0be7ad2..75d6254 100644 --- a/nasse/utils/static.py +++ b/nasse/utils/static.py @@ -1,6 +1,7 @@ """ Helper to create static Python code from Nasse objects """ + import json from nasse.models import Endpoint, Error, Login, Return, UserSent diff --git a/nasse/utils/timer.py b/nasse/utils/timer.py index 1b06dd2..1556ced 100644 --- a/nasse/utils/timer.py +++ b/nasse/utils/timer.py @@ -1,12 +1,21 @@ +""" +A pretty accurate timer to measure code +""" + import time import sys if sys.version_info > (3, 7): - process_time_ns = time.process_time_ns # novermin + process_time_ns = time.process_time_ns # novermin else: - process_time_ns = lambda: int(time.process_time() * 1e+9) + def process_time_ns(): return int(time.process_time() * 1e+9) + class Timer(): + """ + A pretty accurate timer + """ + def __init__(self) -> None: """ A timer to measure performance of a piece of code