Merge branch 'master' into dev

This commit is contained in:
Scoder12
2020-08-05 18:36:45 -07:00
committed by GitHub
7 changed files with 119 additions and 101 deletions
+18
View File
@@ -0,0 +1,18 @@
from replit import maqpy
app = maqpy.App(__name__)
@app.route("/")
@maqpy.authed_ratelimit(
max_requests=1, # Number of requests allowed
period=1, # Amount of time before counter resets
login_res=maqpy.Page(body=f"Sign in\n{maqpy.sign_in_snippet}"),
get_ratelimited_res=(lambda left: f"Too many requests, try again after {left} sec"),
)
def index():
return "You can request this page once per second"
if __name__ == "__main__":
app.run()
@@ -4,14 +4,14 @@ app = maqpy.App(__name__)
@app.route("/")
@maqpy.needs_signin(login_html=f"Hello! {maqpy.sign_in_snippet}")
@maqpy.needs_sign_in(login_res=f"Hello! {maqpy.sign_in_snippet}")
def index():
return "Index function"
# needs_signin can also be called with no args
@app.route("/test")
@maqpy.needs_signin
@maqpy.needs_sign_in
def test():
return "Test function"
Generated
-75
View File
@@ -24,25 +24,6 @@ optional = false
python-versions = "*"
version = "0.7.12"
[[package]]
category = "main"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
name = "anyio"
optional = false
python-versions = ">=3.5.3"
version = "1.4.0"
[package.dependencies]
async-generator = "*"
idna = ">=2.8"
sniffio = ">=1.1"
[package.extras]
curio = ["curio (>=0.9)"]
doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["coverage (>=4.5)", "hypothesis (>=4.0)", "pytest (>=3.7.2)", "uvloop"]
trio = ["trio (>=0.12)"]
[[package]]
category = "dev"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
@@ -51,27 +32,6 @@ optional = false
python-versions = "*"
version = "1.4.4"
[[package]]
category = "main"
description = "asks - async http"
name = "asks"
optional = false
python-versions = "*"
version = "2.4.8"
[package.dependencies]
anyio = "<2"
async_generator = "*"
h11 = "*"
[[package]]
category = "main"
description = "Async generators and context managers for Python 3.5+"
name = "async-generator"
optional = false
python-versions = ">=3.5"
version = "1.10"
[[package]]
category = "main"
description = "Timeout context manager for asyncio programs"
@@ -325,14 +285,6 @@ version = "3.1.7"
[package.dependencies]
gitdb = ">=4.0.1,<5"
[[package]]
category = "main"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
name = "h11"
optional = false
python-versions = "*"
version = "0.9.0"
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
@@ -532,14 +484,6 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.0.4"
[[package]]
category = "main"
description = "Sniff out which async library your code is running under"
name = "sniffio"
optional = false
python-versions = ">=3.5"
version = "1.1.0"
[[package]]
category = "dev"
description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms."
@@ -774,21 +718,10 @@ alabaster = [
{file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
]
anyio = [
{file = "anyio-1.4.0-py3-none-any.whl", hash = "sha256:9ee67e8131853f42957e214d4531cee6f2b66dda164a298d9686a768b7161a4f"},
{file = "anyio-1.4.0.tar.gz", hash = "sha256:95f60964fc4583f3f226f8dc275dfb02aefe7b39b85a999c6d14f4ec5323c1d8"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
asks = [
{file = "asks-2.4.8.tar.gz", hash = "sha256:dcf3b0e80b185430cac1e563a97b2630062893adbb73655d5e8600c83ab63342"},
]
async-generator = [
{file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"},
{file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"},
]
async-timeout = [
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
@@ -875,10 +808,6 @@ gitpython = [
{file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"},
{file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"},
]
h11 = [
{file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"},
{file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
@@ -1041,10 +970,6 @@ smmap = [
{file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"},
{file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"},
]
sniffio = [
{file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"},
{file = "sniffio-1.1.0.tar.gz", hash = "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"},
]
snowballstemmer = [
{file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
-1
View File
@@ -14,7 +14,6 @@ python = "^3.8"
typing_extensions = "^3.7.4"
flask = "^1.1.2"
werkzeug = "^1.0.1"
asks = "^2.4.8"
aiohttp = "^3.6.2"
nest_asyncio = "^1.4.0"
+9 -17
View File
@@ -9,7 +9,15 @@ from . import html
from .app import App
from .files import File
from .html import HTMLElement, Link, Page, Paragraph
from .utils import needs_params, needs_signin, sign_in_snippet, signin
from .utils import (
authed_ratelimit,
local_redirect,
needs_params,
needs_sign_in,
sign_in,
sign_in_page,
sign_in_snippet,
)
from ..database import db
auth = LocalProxy(lambda: flask.request.auth)
@@ -17,19 +25,3 @@ signed_in = LocalProxy(lambda: flask.request.signed_in)
request = LocalProxy(lambda: flask.request)
render_template = flask.render_template
redirect = flask.redirect
def local_redirect(location: str, code: int = 302) -> flask.Response:
"""Perform a redirection to a local path without downgrading to HTTP.
Args:
location (str): The path to redirect to.
code (int): The code to use for the redirect. Defaults to 302.
Returns:
flask.Response: The redirect response.
"""
# Use a LocalProxy so that it can be called before the request context is available
return LocalProxy(
lambda: redirect("https://" + request.headers["host"] + location, code)
)
+2 -2
View File
@@ -5,7 +5,7 @@ from typing import Any, Callable, Set
import flask
from .utils import signin
from .utils import sign_in
@dataclass
@@ -84,7 +84,7 @@ class App(flask.Flask):
not provided, defaults to maqpy.signin()
"""
self._lw_exclude = set(exclude) or set()
self._lw_handler = handler or (lambda: signin())
self._lw_handler = handler or (lambda: sign_in())
def _request_handler(self, rule: str, view_func: Callable) -> Callable:
"""Return a handler for a given request.
+88 -4
View File
@@ -1,8 +1,10 @@
"""Utitilities to make development easier."""
from functools import wraps
import time
from typing import Any, Callable, Union
import flask
from werkzeug.local import LocalProxy
from .html import Page
@@ -13,7 +15,7 @@ sign_in_snippet = (
)
def signin(title: str = "Please Sign In") -> Page:
def sign_in(title: str = "Please Sign In") -> Page:
"""Return a sign-in page.
Args:
@@ -25,13 +27,16 @@ def signin(title: str = "Please Sign In") -> Page:
return Page(title=title, body=sign_in_snippet)
def needs_signin(func: Callable = None, login_html: str = sign_in_snippet) -> Callable:
sign_in_page = sign_in()
def needs_sign_in(func: Callable = None, login_res: str = sign_in_page) -> Callable:
"""A decorator that enforces that the user is signed in before accessing the page.
Args:
func (Callable): The function passed in if used as a decorator. Defaults to
None.
login_html (str): The HTML to show when the user needs to sign in. Defaults to
login_res (str): The HTML to show when the user needs to sign in. Defaults to
sign_in_snippet.
Returns:
@@ -44,7 +49,7 @@ def needs_signin(func: Callable = None, login_html: str = sign_in_snippet) -> Ca
if flask.request.signed_in:
return func(*args, **kwargs)
else:
return login_html
return login_res
return handler
@@ -112,3 +117,82 @@ def needs_params(
return handler
return decorator
def local_redirect(location: str, code: int = 302) -> flask.Response:
"""Perform a redirection to a local path without downgrading to HTTP.
Args:
location (str): The path to redirect to.
code (int): The code to use for the redirect. Defaults to 302.
Returns:
flask.Response: The redirect response.
"""
# Use a LocalProxy so that it can be called before the request context is available
return LocalProxy(
lambda: flask.redirect(
"https://" + flask.request.headers["host"] + location, code
)
)
def authed_ratelimit(
max_requests: int,
period: float,
login_res: str = sign_in_page,
get_ratelimited_res: Callable[[float], str] = (
lambda left: f"Too many requests, wait {left} sec"
),
) -> Callable[[Callable], flask.Response]:
"""Require sign in and limit the amount of requests each signed in user can perform.
This decorator also calls needs_signin for you and passes the login_res kwarg
directly to it.
Args:
max_requests (int): The maximum amount of requests allowed in the period.
period (float): The length of the period.
login_res (str): The response to be shown if the user is not signed in, passed
to needs_sign_in.
get_ratelimited_res (Callable[[float], str]): A callable which is passed the
amount of time remaining before the user can request again and returns the
response that should be sent to the user.
Returns:
Callable[[Callable], flask.Response]: A function which decorates the handler.
"""
def decorator(func: Callable) -> flask.Response:
last_reset = time.time()
num_requests = {}
# Checks for signin first, before checking ratelimit
@needs_sign_in(login_res=login_res)
@wraps(func)
def handler(*args: Any, **kwargs: Any) -> flask.Response:
nonlocal last_reset
nonlocal num_requests
name = flask.request.auth.name
now = time.time()
if now - last_reset >= period:
last_reset = now
num_requests = {}
times_requested = num_requests.get(name, 0)
if times_requested >= max_requests:
res = get_ratelimited_res(period - (now - last_reset))
# Make a reponse object so that status can be set
if not isinstance(res, flask.Response):
res = flask.make_response(res)
res.status = "429"
return res
num_requests[name] = times_requested + 1
return func(*args, **kwargs)
return handler
return decorator