kopia lustrzana https://github.com/cirospaciari/socketify.py
update uSockets and projects
rodzic
59619dd697
commit
291af489f1
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "socketify"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
authors = [
|
||||
{ name="Ciro Spaciari", email="ciro.spaciari@gmail.com" },
|
||||
]
|
||||
|
|
2
setup.py
2
setup.py
|
@ -58,7 +58,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|||
|
||||
setuptools.setup(
|
||||
name="socketify",
|
||||
version="0.0.6",
|
||||
version="0.0.7",
|
||||
platforms=["any"],
|
||||
author="Ciro Spaciari",
|
||||
author_email="ciro.spaciari@gmail.com",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import asyncio
|
||||
|
||||
from .socketify import (
|
||||
App,
|
||||
AppOptions,
|
||||
|
|
|
@ -8,7 +8,10 @@ import sys
|
|||
import logging
|
||||
import uuid
|
||||
import asyncio
|
||||
|
||||
is_pypy = platform.python_implementation() == "PyPy"
|
||||
|
||||
|
||||
async def task_wrapper(task):
|
||||
try:
|
||||
return await task
|
||||
|
@ -19,6 +22,7 @@ async def task_wrapper(task):
|
|||
finally:
|
||||
return None
|
||||
|
||||
|
||||
EMPTY_RESPONSE = {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
|
||||
|
@ -162,8 +166,10 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
|
|||
_id = uuid.uuid4()
|
||||
|
||||
app.server._socket_refs[_id] = ws
|
||||
|
||||
def unregister():
|
||||
app.server._socket_refs.pop(_id, None)
|
||||
|
||||
ws.unregister = unregister
|
||||
lib.uws_res_upgrade(
|
||||
ssl,
|
||||
|
@ -495,7 +501,15 @@ def asgi(ssl, response, info, user_data, aborted):
|
|||
|
||||
|
||||
class _ASGI:
|
||||
def __init__(self, app, options=None, websocket=True, websocket_options=None, task_factory_max_items=100_000, lifespan=True):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
options=None,
|
||||
websocket=True,
|
||||
websocket_options=None,
|
||||
task_factory_max_items=100_000,
|
||||
lifespan=True,
|
||||
):
|
||||
self.server = App(options, task_factory_max_items=0)
|
||||
self.SERVER_PORT = None
|
||||
self.SERVER_HOST = ""
|
||||
|
@ -514,27 +528,35 @@ class _ASGI:
|
|||
def run_task(task):
|
||||
factory(loop, task_wrapper(task))
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
else:
|
||||
|
||||
def run_task(task):
|
||||
create_task(loop, task_wrapper(task))
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
|
||||
else:
|
||||
if sys.version_info >= (3, 8): # name fixed to avoid dynamic name
|
||||
|
||||
def run_task(task):
|
||||
future = loop.create_task(task_wrapper(task), name='socketify.py-request-task')
|
||||
future = loop.create_task(
|
||||
task_wrapper(task), name="socketify.py-request-task"
|
||||
)
|
||||
future._log_destroy_pending = False
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
else:
|
||||
|
||||
def run_task(task):
|
||||
future = loop.create_task(task_wrapper(task))
|
||||
future._log_destroy_pending = False
|
||||
loop._run_once()
|
||||
self._run_task = run_task
|
||||
|
||||
self._run_task = run_task
|
||||
|
||||
self.app = app
|
||||
self.ws_compression = False
|
||||
|
@ -686,10 +708,12 @@ class _ASGI:
|
|||
return self
|
||||
# signal stop
|
||||
self.status = 3
|
||||
self.stop_future.set_result({
|
||||
self.stop_future.set_result(
|
||||
{
|
||||
"type": "lifespan.shutdown",
|
||||
"asgi": {"version": "3.0", "spec_version": "2.3"},
|
||||
})
|
||||
}
|
||||
)
|
||||
# run until end or fail
|
||||
while self.status == 3:
|
||||
self.server.loop.run_once()
|
||||
|
@ -714,8 +738,8 @@ class ASGI:
|
|||
options=None,
|
||||
websocket=True,
|
||||
websocket_options=None,
|
||||
task_factory_max_items=100_000, #default = 100k = +20mib in memory
|
||||
lifespan=True
|
||||
task_factory_max_items=100_000, # default = 100k = +20mib in memory
|
||||
lifespan=True,
|
||||
):
|
||||
self.app = app
|
||||
self.options = options
|
||||
|
@ -737,7 +761,7 @@ class ASGI:
|
|||
self.websocket,
|
||||
self.websocket_options,
|
||||
self.task_factory_max_items,
|
||||
self.lifespan
|
||||
self.lifespan,
|
||||
)
|
||||
if self.listen_options:
|
||||
(port_or_options, handler) = self.listen_options
|
||||
|
|
|
@ -2,6 +2,7 @@ import inspect
|
|||
import os
|
||||
import logging
|
||||
from . import App, AppOptions, AppListenOptions
|
||||
|
||||
help = """
|
||||
Usage: python -m socketify APP [OPTIONS]
|
||||
python3 -m socketify APP [OPTIONS]
|
||||
|
@ -43,37 +44,56 @@ Example:
|
|||
|
||||
"""
|
||||
|
||||
# --reload Enable auto-reload. This options also disable --workers or -w option.
|
||||
# --reload-dir PATH Set reload directories explicitly, instead of using the current working directory.
|
||||
# --reload-include TEXT Set extensions to include while watching for files.
|
||||
# Includes '.py,.html,.js,.png,.jpeg,.jpg and .webp' by default;
|
||||
# these defaults can be overridden with `--reload-exclude`.
|
||||
# --reload-exclude TEXT Set extensions to include while watching for files.
|
||||
# --reload-delay INT Milliseconds to delay reload between file changes. [default: 1000]
|
||||
# --reload Enable auto-reload. This options also disable --workers or -w option.
|
||||
# --reload-dir PATH Set reload directories explicitly, instead of using the current working directory.
|
||||
# --reload-include TEXT Set extensions to include while watching for files.
|
||||
# Includes '.py,.html,.js,.png,.jpeg,.jpg and .webp' by default;
|
||||
# these defaults can be overridden with `--reload-exclude`.
|
||||
# --reload-exclude TEXT Set extensions to include while watching for files.
|
||||
# --reload-delay INT Milliseconds to delay reload between file changes. [default: 1000]
|
||||
def is_wsgi(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 2
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 2
|
||||
)
|
||||
|
||||
|
||||
def is_asgi(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
|
||||
)
|
||||
|
||||
|
||||
def is_ssgi(module):
|
||||
return False # no spec yet
|
||||
|
||||
|
||||
def is_ssgi(module):
|
||||
return False # no spec yet
|
||||
|
||||
|
||||
def is_socketify(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 1
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 1
|
||||
)
|
||||
|
||||
|
||||
def is_factory(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 0
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 0
|
||||
)
|
||||
|
||||
|
||||
def str_bool(text):
|
||||
text = str(text).lower()
|
||||
return text == "true"
|
||||
|
||||
|
||||
def load_module(file, reload=False):
|
||||
try:
|
||||
[full_module, app] = file.split(':')
|
||||
[full_module, app] = file.split(":")
|
||||
import importlib
|
||||
module =importlib.import_module(full_module)
|
||||
|
||||
module = importlib.import_module(full_module)
|
||||
if reload:
|
||||
importlib.reload(module)
|
||||
|
||||
|
@ -85,6 +105,8 @@ def load_module(file, reload=False):
|
|||
except Exception as error:
|
||||
logging.exception(error)
|
||||
return None
|
||||
|
||||
|
||||
def execute(args):
|
||||
arguments_length = len(args)
|
||||
if arguments_length <= 2:
|
||||
|
@ -93,8 +115,11 @@ def execute(args):
|
|||
if arguments_length == 2 and (args[1] == "--version" or args[1] == "-v"):
|
||||
import pkg_resources # part of setuptools
|
||||
import platform
|
||||
|
||||
version = pkg_resources.require("socketify")[0].version
|
||||
return print(f"Running socketify {version} with {platform.python_implementation()} {platform.python_version()} on {platform.system()}")
|
||||
return print(
|
||||
f"Running socketify {version} with {platform.python_implementation()} {platform.python_version()} on {platform.system()}"
|
||||
)
|
||||
elif arguments_length < 2:
|
||||
return print(help)
|
||||
|
||||
|
@ -111,7 +136,7 @@ def execute(args):
|
|||
if selected_option:
|
||||
options[selected_option] = option
|
||||
selected_option = None
|
||||
elif option.startswith('--') or option.startswith('-'):
|
||||
elif option.startswith("--") or option.startswith("-"):
|
||||
if selected_option is None:
|
||||
selected_option = option
|
||||
else: # --factory, --reload etc
|
||||
|
@ -126,31 +151,40 @@ def execute(args):
|
|||
if interface == "auto":
|
||||
if is_asgi(module):
|
||||
from . import ASGI as Interface
|
||||
|
||||
interface = "asgi"
|
||||
elif is_wsgi(module):
|
||||
from . import WSGI as Interface
|
||||
|
||||
interface = "wsgi"
|
||||
elif is_ssgi(module):
|
||||
from . import SSGI as Interface
|
||||
|
||||
interface = "ssgi"
|
||||
else:
|
||||
interface = "socketify"
|
||||
|
||||
elif interface == "asgi" or interface == "asgi3":
|
||||
from . import ASGI as Interface
|
||||
|
||||
interface = "asgi"
|
||||
# you may use ASGI in SSGI so no checks here
|
||||
if is_wsgi(module):
|
||||
return print("Cannot use WSGI interface as ASGI interface")
|
||||
if not is_asgi(module):
|
||||
return print("ASGI interface must be callable with 3 parameters async def app(scope, receive and send)")
|
||||
return print(
|
||||
"ASGI interface must be callable with 3 parameters async def app(scope, receive and send)"
|
||||
)
|
||||
elif interface == "wsgi":
|
||||
from . import WSGI as Interface
|
||||
|
||||
# you may use WSGI in SSGI so no checks here
|
||||
if is_asgi(module):
|
||||
return print("Cannot use ASGI interface as WSGI interface")
|
||||
if not is_wsgi(module):
|
||||
return print("WSGI interface must be callable with 2 parameters def app(environ, start_response)")
|
||||
return print(
|
||||
"WSGI interface must be callable with 2 parameters def app(environ, start_response)"
|
||||
)
|
||||
|
||||
elif interface == "ssgi":
|
||||
# if not is_ssgi(module):
|
||||
|
@ -161,15 +195,19 @@ def execute(args):
|
|||
elif interface != "socketify":
|
||||
return print(f"{interface} interface is not supported yet")
|
||||
|
||||
auto_reload = options.get('--reload', False)
|
||||
workers = int(options.get("--workers", options.get("-w", os.environ.get('WEB_CONCURRENCY', 1))))
|
||||
auto_reload = options.get("--reload", False)
|
||||
workers = int(
|
||||
options.get(
|
||||
"--workers", options.get("-w", os.environ.get("WEB_CONCURRENCY", 1))
|
||||
)
|
||||
)
|
||||
if workers < 1 or auto_reload:
|
||||
workers = 1
|
||||
|
||||
port = int(options.get("--port", options.get("-p", 8000)))
|
||||
host = options.get("--host", options.get("-h", "127.0.0.1"))
|
||||
uds = options.get('--uds', None)
|
||||
lifespan = options.get('--lifespan', "auto")
|
||||
uds = options.get("--uds", None)
|
||||
lifespan = options.get("--lifespan", "auto")
|
||||
lifespan = False if lifespan == "off" else True
|
||||
task_factory_maxitems = int(options.get("--task-factory-maxitems", 100000))
|
||||
|
||||
|
@ -189,22 +227,21 @@ def execute(args):
|
|||
else: # if is socketify websockets must be set in app
|
||||
websockets = False
|
||||
else:
|
||||
#websocket dedicated module
|
||||
# websocket dedicated module
|
||||
ws_module = load_module(websockets)
|
||||
if not ws_module:
|
||||
return print(f"Cannot load websocket module {websockets}")
|
||||
websockets = ws_module
|
||||
|
||||
|
||||
key_file_name = options.get('--ssl-keyfile', None)
|
||||
key_file_name = options.get("--ssl-keyfile", None)
|
||||
|
||||
if key_file_name:
|
||||
ssl_options = AppOptions(
|
||||
key_file_name=options.get('--ssl-keyfile', None),
|
||||
cert_file_name=options.get('--ssl-certfile', None),
|
||||
passphrase=options.get('--ssl-keyfile-password', None),
|
||||
ca_file_name=options.get('--ssl-ca-certs', None),
|
||||
ssl_ciphers=options.get('--ssl-ciphers', None)
|
||||
key_file_name=options.get("--ssl-keyfile", None),
|
||||
cert_file_name=options.get("--ssl-certfile", None),
|
||||
passphrase=options.get("--ssl-keyfile-password", None),
|
||||
ca_file_name=options.get("--ssl-ca-certs", None),
|
||||
ssl_ciphers=options.get("--ssl-ciphers", None),
|
||||
)
|
||||
else:
|
||||
ssl_options = None
|
||||
|
@ -212,20 +249,30 @@ def execute(args):
|
|||
def listen_log(config):
|
||||
if not disable_listen_log:
|
||||
if uds:
|
||||
print(f"Listening on {config.domain} {'https' if ssl_options else 'http'}://localhost now\n")
|
||||
print(
|
||||
f"Listening on {config.domain} {'https' if ssl_options else 'http'}://localhost now\n"
|
||||
)
|
||||
else:
|
||||
print(f"Listening on {'https' if ssl_options else 'http'}://{config.host if config.host and len(config.host) > 1 else '127.0.0.1' }:{config.port} now\n")
|
||||
print(
|
||||
f"Listening on {'https' if ssl_options else 'http'}://{config.host if config.host and len(config.host) > 1 else '127.0.0.1' }:{config.port} now\n"
|
||||
)
|
||||
|
||||
if websockets:
|
||||
websocket_options = {
|
||||
'compression': int(1 if options.get('--ws-per-message-deflate', False) else 0),
|
||||
'max_payload_length': int(options.get('--ws-max-size', 16777216)),
|
||||
'idle_timeout': int(options.get('--ws-idle-timeout', 20)),
|
||||
'send_pings_automatically': str_bool(options.get('--ws-auto-ping', True)),
|
||||
'reset_idle_timeout_on_send': str_bool(options.get('--ws-reset-idle-on-send', True)),
|
||||
'max_lifetime': int(options.get('--ws-max-lifetime', 0)),
|
||||
'max_backpressure': int(options.get('--max-backpressure', 16777216)),
|
||||
'close_on_backpressure_limit': str_bool(options.get('--ws-close-on-backpressure-limit', False))
|
||||
"compression": int(
|
||||
1 if options.get("--ws-per-message-deflate", False) else 0
|
||||
),
|
||||
"max_payload_length": int(options.get("--ws-max-size", 16777216)),
|
||||
"idle_timeout": int(options.get("--ws-idle-timeout", 20)),
|
||||
"send_pings_automatically": str_bool(options.get("--ws-auto-ping", True)),
|
||||
"reset_idle_timeout_on_send": str_bool(
|
||||
options.get("--ws-reset-idle-on-send", True)
|
||||
),
|
||||
"max_lifetime": int(options.get("--ws-max-lifetime", 0)),
|
||||
"max_backpressure": int(options.get("--max-backpressure", 16777216)),
|
||||
"close_on_backpressure_limit": str_bool(
|
||||
options.get("--ws-close-on-backpressure-limit", False)
|
||||
),
|
||||
}
|
||||
else:
|
||||
websocket_options = None
|
||||
|
@ -238,13 +285,23 @@ def execute(args):
|
|||
elif is_wsgi(module):
|
||||
return print("Cannot use WSGI interface as socketify interface")
|
||||
elif not is_socketify(module):
|
||||
return print("socketify interface must be callable with 1 parameter def run(app: App)")
|
||||
return print(
|
||||
"socketify interface must be callable with 1 parameter def run(app: App)"
|
||||
)
|
||||
# run app with the settings desired
|
||||
def run_app():
|
||||
fork_app = App(ssl_options, int(options.get("--req-res-factory-maxitems", 0)), int(options.get("--ws-factory-maxitems", 0)), task_factory_maxitems, lifespan)
|
||||
fork_app = App(
|
||||
ssl_options,
|
||||
int(options.get("--req-res-factory-maxitems", 0)),
|
||||
int(options.get("--ws-factory-maxitems", 0)),
|
||||
task_factory_maxitems,
|
||||
lifespan,
|
||||
)
|
||||
module(fork_app) # call module factory
|
||||
|
||||
if websockets: # if socketify websockets are added using --ws in socketify interface we can set here
|
||||
if (
|
||||
websockets
|
||||
): # if socketify websockets are added using --ws in socketify interface we can set here
|
||||
websockets.update(websocket_options) # set websocket options
|
||||
fork_app.ws("/*", websockets)
|
||||
if uds:
|
||||
|
@ -268,11 +325,28 @@ def execute(args):
|
|||
run_app()
|
||||
# sigint everything to gracefull shutdown
|
||||
import signal
|
||||
|
||||
for pid in pid_list:
|
||||
os.kill(pid, signal.SIGINT)
|
||||
else:
|
||||
|
||||
if uds:
|
||||
Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options, task_factory_max_items=task_factory_maxitems, lifespan=lifespan).listen(AppListenOptions(domain=uds), listen_log).run(workers=workers)
|
||||
Interface(
|
||||
module,
|
||||
options=ssl_options,
|
||||
websocket=websockets,
|
||||
websocket_options=websocket_options,
|
||||
task_factory_max_items=task_factory_maxitems,
|
||||
lifespan=lifespan,
|
||||
).listen(AppListenOptions(domain=uds), listen_log).run(workers=workers)
|
||||
else:
|
||||
Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options, task_factory_max_items=task_factory_maxitems, lifespan=lifespan).listen(AppListenOptions(port=port, host=host), listen_log).run(workers=workers)
|
||||
Interface(
|
||||
module,
|
||||
options=ssl_options,
|
||||
websocket=websockets,
|
||||
websocket_options=websocket_options,
|
||||
task_factory_max_items=task_factory_maxitems,
|
||||
lifespan=lifespan,
|
||||
).listen(AppListenOptions(port=port, host=host), listen_log).run(
|
||||
workers=workers
|
||||
)
|
||||
|
|
|
@ -55,8 +55,14 @@ async def sendfile(res, req, filename):
|
|||
fd.seek(start)
|
||||
if start > total_size or size > total_size or size < 0 or start < 0:
|
||||
if content_type:
|
||||
return res.cork(lambda res: res.write_header(b"Content-Type", content_type).write_status(416).end_without_body())
|
||||
return res.cork(lambda res: res.write_status(416).end_without_body())
|
||||
return res.cork(
|
||||
lambda res: res.write_header(b"Content-Type", content_type)
|
||||
.write_status(416)
|
||||
.end_without_body()
|
||||
)
|
||||
return res.cork(
|
||||
lambda res: res.write_status(416).end_without_body()
|
||||
)
|
||||
status = 206
|
||||
else:
|
||||
end = size - 1
|
||||
|
@ -74,6 +80,7 @@ async def sendfile(res, req, filename):
|
|||
res.write_header(
|
||||
b"Content-Range", "bytes %d-%d/%d" % (start, end, total_size)
|
||||
)
|
||||
|
||||
res.cork(send_headers)
|
||||
pending_size = size
|
||||
# keep sending until abort or done
|
||||
|
@ -104,7 +111,7 @@ def static_route(app, route, directory):
|
|||
def route_handler(res, req):
|
||||
url = req.get_url()
|
||||
res.grab_aborted_handler()
|
||||
url = url[len(route)::]
|
||||
url = url[len(route) : :]
|
||||
if url.endswith("/"):
|
||||
if url.startswith("/"):
|
||||
url = url[1:-1]
|
||||
|
@ -123,6 +130,7 @@ def static_route(app, route, directory):
|
|||
route = route[:-1]
|
||||
app.get("%s/*" % route, route_handler)
|
||||
|
||||
|
||||
def middleware(*functions):
|
||||
syncs = []
|
||||
asyncs = []
|
||||
|
@ -146,6 +154,7 @@ def middleware(*functions):
|
|||
# stops if returns Falsy
|
||||
if not data:
|
||||
return
|
||||
|
||||
async def wrapper(res, req, data):
|
||||
# cicle to all middlewares
|
||||
for function in asyncs:
|
||||
|
@ -170,7 +179,6 @@ def middleware(*functions):
|
|||
return optimized_middleware_route
|
||||
|
||||
|
||||
|
||||
def sync_middleware(*functions):
|
||||
# we use Optional data=None at the end so you can use and middleware inside a middleware
|
||||
def middleware_route(res, req, data=None):
|
||||
|
@ -185,6 +193,7 @@ def sync_middleware(*functions):
|
|||
|
||||
return middleware_route
|
||||
|
||||
|
||||
def async_middleware(*functions):
|
||||
# we use Optional data=None at the end so you can use and middleware inside a middleware
|
||||
async def middleware_route(res, req, data=None):
|
||||
|
@ -211,7 +220,7 @@ def async_middleware(*functions):
|
|||
|
||||
|
||||
class DecoratorRouter:
|
||||
def __init__(self, app, prefix: str="", *middlewares):
|
||||
def __init__(self, app, prefix: str = "", *middlewares):
|
||||
self.app = app
|
||||
self.middlewares = middlewares
|
||||
self.prefix = prefix
|
||||
|
@ -232,6 +241,7 @@ class DecoratorRouter:
|
|||
|
||||
def post(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -244,6 +254,7 @@ class DecoratorRouter:
|
|||
|
||||
def options(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -256,6 +267,7 @@ class DecoratorRouter:
|
|||
|
||||
def delete(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -268,6 +280,7 @@ class DecoratorRouter:
|
|||
|
||||
def patch(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -280,6 +293,7 @@ class DecoratorRouter:
|
|||
|
||||
def put(self, path: str):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -292,6 +306,7 @@ class DecoratorRouter:
|
|||
|
||||
def head(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -304,6 +319,7 @@ class DecoratorRouter:
|
|||
|
||||
def connect(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -316,6 +332,7 @@ class DecoratorRouter:
|
|||
|
||||
def trace(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -328,6 +345,7 @@ class DecoratorRouter:
|
|||
|
||||
def any(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(*self.middlewares)
|
||||
|
@ -338,6 +356,7 @@ class DecoratorRouter:
|
|||
|
||||
return decorator
|
||||
|
||||
|
||||
class MiddlewareRouter:
|
||||
def __init__(self, app, *middlewares):
|
||||
self.app = app
|
||||
|
|
|
@ -6,7 +6,9 @@ from .uv import UVLoop
|
|||
import asyncio
|
||||
import platform
|
||||
|
||||
is_pypy = platform.python_implementation() == 'PyPy'
|
||||
is_pypy = platform.python_implementation() == "PyPy"
|
||||
|
||||
|
||||
async def task_wrapper(exception_handler, loop, response, task):
|
||||
try:
|
||||
return await task
|
||||
|
@ -52,6 +54,7 @@ class Loop:
|
|||
# custom task factory
|
||||
def pypy_task_factory(loop, coro, context=None):
|
||||
return create_task(loop, coro, context=context)
|
||||
|
||||
self.loop.set_task_factory(pypy_task_factory)
|
||||
else:
|
||||
# CPython performs worse using custom create_task, so native create_task is used
|
||||
|
@ -60,7 +63,6 @@ class Loop:
|
|||
|
||||
self.run_async = self._run_async_cpython
|
||||
|
||||
|
||||
def set_timeout(self, timeout, callback, user_data):
|
||||
return self.uv_loop.create_timer(timeout, 0, callback, user_data)
|
||||
|
||||
|
@ -110,7 +112,6 @@ class Loop:
|
|||
# run one step of libuv
|
||||
self.uv_loop.run_once()
|
||||
|
||||
|
||||
def stop(self):
|
||||
if self.started:
|
||||
# Just mark as started = False and wait
|
||||
|
@ -121,12 +122,13 @@ class Loop:
|
|||
def get_native_loop(self):
|
||||
return self.uv_loop.get_native_loop()
|
||||
|
||||
|
||||
def _run_async_pypy(self, task, response=None):
|
||||
# this garanties error 500 in case of uncaught exceptions, and can trigger the custom error handler
|
||||
# using an coroutine wrapper generates less overhead than using add_done_callback
|
||||
# this is an custom task/future with less overhead
|
||||
future = self._task_factory(self.loop, task_wrapper(self.exception_handler, self.loop, response, task))
|
||||
future = self._task_factory(
|
||||
self.loop, task_wrapper(self.exception_handler, self.loop, response, task)
|
||||
)
|
||||
# force asyncio run once to enable req in async functions before first await
|
||||
self.loop._run_once()
|
||||
return None # this future maybe already done and reused not safe to await
|
||||
|
@ -134,15 +136,19 @@ class Loop:
|
|||
def _run_async_cpython(self, task, response=None):
|
||||
# this garanties error 500 in case of uncaught exceptions, and can trigger the custom error handler
|
||||
# using an coroutine wrapper generates less overhead than using add_done_callback
|
||||
future = self.loop.create_task(task_wrapper(self.exception_handler, self.loop, response, task))
|
||||
future = self.loop.create_task(
|
||||
task_wrapper(self.exception_handler, self.loop, response, task)
|
||||
)
|
||||
# force asyncio run once to enable req in async functions before first await
|
||||
self.loop._run_once()
|
||||
return None # this future is safe to await but we return None for compatibility, and in the future will be the same behavior as PyPy
|
||||
|
||||
def dispose(self):
|
||||
if self.uv_loop:
|
||||
self.uv_loop.dispose()
|
||||
self.uv_loop = None
|
||||
|
||||
|
||||
# if sys.version_info >= (3, 11)
|
||||
# with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
|
||||
# runner.run(main())
|
||||
|
|
|
@ -401,5 +401,3 @@ library_path = os.path.join(
|
|||
|
||||
|
||||
lib = ffi.dlopen(library_path)
|
||||
|
||||
|
||||
|
|
|
@ -4,9 +4,6 @@ from http import cookies
|
|||
import inspect
|
||||
from io import BytesIO
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import uuid
|
||||
from urllib.parse import parse_qs, quote_plus, unquote_plus
|
||||
|
@ -20,8 +17,6 @@ from dataclasses import dataclass
|
|||
from .helpers import DecoratorRouter
|
||||
from typing import Union
|
||||
|
||||
mimetypes.init()
|
||||
|
||||
|
||||
@ffi.callback("void(const char*, size_t, void*)")
|
||||
def uws_missing_server_name(hostname, hostname_length, user_data):
|
||||
|
@ -1772,7 +1767,7 @@ class AppResponse:
|
|||
else:
|
||||
data = self.app._json_serializer.dumps(message).encode("utf-8")
|
||||
# ignores content_type should always be json here
|
||||
self.write_header(b"Content-Type", b'application/json')
|
||||
self.write_header(b"Content-Type", b"application/json")
|
||||
|
||||
lib.uws_res_end(
|
||||
self.app.SSL, self.res, data, len(data), 1 if end_connection else 0
|
||||
|
@ -2476,7 +2471,7 @@ class App:
|
|||
request_response_factory_max_items=0,
|
||||
websocket_factory_max_items=0,
|
||||
task_factory_max_items=100_000,
|
||||
lifespan=True
|
||||
lifespan=True,
|
||||
):
|
||||
socket_options_ptr = ffi.new("struct us_socket_context_options_t *")
|
||||
socket_options = socket_options_ptr[0]
|
||||
|
@ -3128,6 +3123,7 @@ class App:
|
|||
|
||||
def listen(self, port_or_options=None, handler=None):
|
||||
if self.lifespan:
|
||||
|
||||
async def task_wrapper(task):
|
||||
try:
|
||||
if inspect.iscoroutinefunction(task):
|
||||
|
@ -3229,6 +3225,7 @@ class App:
|
|||
signal.signal(signal.SIGINT, lambda sig, frame: self.close())
|
||||
self.loop.run()
|
||||
if self.lifespan:
|
||||
|
||||
async def task_wrapper(task):
|
||||
try:
|
||||
if inspect.iscoroutinefunction(task):
|
||||
|
@ -3240,6 +3237,7 @@ class App:
|
|||
self.trigger_error(error, None, None)
|
||||
finally:
|
||||
return None
|
||||
|
||||
# shutdown lifespan
|
||||
if self._on_shutdown_handler:
|
||||
self.loop.run_until_complete(task_wrapper(self._on_shutdown_handler))
|
||||
|
@ -3276,7 +3274,6 @@ class App:
|
|||
else:
|
||||
try:
|
||||
if inspect.iscoroutinefunction(self.error_handler):
|
||||
print("coroutine!", error)
|
||||
self.run_async(
|
||||
self.error_handler(error, response, request), response
|
||||
)
|
||||
|
|
|
@ -89,7 +89,9 @@ class RequestTask:
|
|||
# status is still pending
|
||||
_log_destroy_pending = True
|
||||
|
||||
def __init__(self, coro, loop, default_done_callback=None, no_register=False, context=None):
|
||||
def __init__(
|
||||
self, coro, loop, default_done_callback=None, no_register=False, context=None
|
||||
):
|
||||
"""Initialize the future.
|
||||
|
||||
The optional event_loop argument allows explicitly setting the event
|
||||
|
@ -590,10 +592,12 @@ def create_task_with_factory(task_factory_max_items=100_000):
|
|||
if len(items) == 0:
|
||||
return create_task(loop, coro, default_done_callback)
|
||||
task = items.pop()
|
||||
|
||||
def done(f):
|
||||
if default_done_callback is not None:
|
||||
default_done_callback(f)
|
||||
items.append(f)
|
||||
|
||||
task._reuse(coro, loop, done)
|
||||
return task
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 9b13a6b02886c792189252e948290d2eea9aeda9
|
||||
Subproject commit 7187fc3d658d4335cdf0c79371eeb8310717b95c
|
|
@ -7,6 +7,7 @@ def socketify_generic_handler(data):
|
|||
(handler, user_data) = ffi.from_handle(data)
|
||||
handler(user_data)
|
||||
|
||||
|
||||
class UVCheck:
|
||||
def __init__(self, loop, handler, user_data):
|
||||
self._handler_data = ffi.new_handle((handler, user_data))
|
||||
|
|
|
@ -5,11 +5,13 @@ from .asgi import ws_close, ws_upgrade, ws_open, ws_message
|
|||
from io import BytesIO, BufferedReader
|
||||
from .native import lib, ffi
|
||||
import platform
|
||||
|
||||
is_pypy = platform.python_implementation() == "PyPy"
|
||||
from .tasks import create_task, create_task_with_factory
|
||||
import sys
|
||||
import logging
|
||||
|
||||
|
||||
@ffi.callback("void(uws_res_t*, const char*, size_t, bool, void*)")
|
||||
def wsgi_on_data_handler(res, chunk, chunk_length, is_end, user_data):
|
||||
data_response = ffi.from_handle(user_data)
|
||||
|
@ -23,6 +25,7 @@ def wsgi_on_data_handler(res, chunk, chunk_length, is_end, user_data):
|
|||
data_response._ptr,
|
||||
)
|
||||
|
||||
|
||||
class WSGIBody:
|
||||
def __init__(self, buffer):
|
||||
self.buf = buffer
|
||||
|
@ -106,10 +109,11 @@ class WSGIBody:
|
|||
ret.append(data)
|
||||
data = b""
|
||||
else:
|
||||
line, data = data[:pos + 1], data[pos + 1:]
|
||||
line, data = data[: pos + 1], data[pos + 1 :]
|
||||
ret.append(line)
|
||||
return ret
|
||||
|
||||
|
||||
class WSGIDataResponse:
|
||||
def __init__(self, app, environ, start_response, aborted, buffer, on_data):
|
||||
self.buffer = buffer
|
||||
|
@ -120,6 +124,7 @@ class WSGIDataResponse:
|
|||
self.app = app
|
||||
self.start_response = start_response
|
||||
|
||||
|
||||
@ffi.callback("void(uws_res_t*, void*)")
|
||||
def wsgi_corked_response_start_handler(res, user_data):
|
||||
data_response = ffi.from_handle(user_data)
|
||||
|
@ -158,6 +163,7 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
headers_set = None
|
||||
headers_written = False
|
||||
status_text = None
|
||||
|
||||
def write_headers(headers):
|
||||
nonlocal headers_written, headers_set, status_text
|
||||
if headers_written or not headers_set:
|
||||
|
@ -171,7 +177,6 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
elif isinstance(status_text, bytes):
|
||||
lib.uws_res_write_status(ssl, response, status_text, len(status_text))
|
||||
|
||||
|
||||
for (key, value) in headers:
|
||||
if isinstance(key, str):
|
||||
# this is faster than using .lower()
|
||||
|
@ -194,7 +199,6 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
continue # auto
|
||||
key_data = key
|
||||
|
||||
|
||||
if isinstance(value, str):
|
||||
value_data = value.encode("utf-8")
|
||||
elif isinstance(value, bytes):
|
||||
|
@ -212,6 +216,7 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
lib.uws_res_write_header(
|
||||
ssl, response, key_data, len(key_data), value_data, len(value_data)
|
||||
)
|
||||
|
||||
def start_response(status, headers, exc_info=None):
|
||||
nonlocal headers_set, status_text
|
||||
if exc_info:
|
||||
|
@ -224,7 +229,6 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
elif headers_set:
|
||||
raise AssertionError("Headers already set!")
|
||||
|
||||
|
||||
headers_set = headers
|
||||
status_text = status
|
||||
|
||||
|
@ -237,6 +241,7 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
elif isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
|
||||
return write
|
||||
|
||||
# check for body
|
||||
|
@ -249,7 +254,9 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
return
|
||||
|
||||
ssl = data_response.app.server.SSL
|
||||
data_response.environ["CONTENT_LENGTH"] = str(data_response.buffer.getbuffer().nbytes)
|
||||
data_response.environ["CONTENT_LENGTH"] = str(
|
||||
data_response.buffer.getbuffer().nbytes
|
||||
)
|
||||
app_iter = data_response.app.wsgi(
|
||||
data_response.environ, data_response.start_response
|
||||
)
|
||||
|
@ -302,11 +309,22 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
write_headers(headers_set)
|
||||
lib.uws_res_end_without_body(ssl, response, 0)
|
||||
|
||||
|
||||
def is_asgi(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
|
||||
)
|
||||
|
||||
|
||||
class _WSGI:
|
||||
def __init__(self, app, options=None, websocket=None, websocket_options=None, task_factory_max_items=100_000):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
options=None,
|
||||
websocket=None,
|
||||
websocket_options=None,
|
||||
task_factory_max_items=100_000,
|
||||
):
|
||||
self.server = App(options, task_factory_max_items=0)
|
||||
self.SERVER_HOST = None
|
||||
self.SERVER_PORT = None
|
||||
|
@ -338,23 +356,30 @@ class _WSGI:
|
|||
def run_task(task):
|
||||
factory(loop, task)
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
else:
|
||||
|
||||
def run_task(task):
|
||||
create_task(loop, task)
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
|
||||
else:
|
||||
if sys.version_info >= (3, 8): # name fixed to avoid dynamic name
|
||||
|
||||
def run_task(task):
|
||||
loop.create_task(task, name='socketify.py-request-task')
|
||||
loop.create_task(task, name="socketify.py-request-task")
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
else:
|
||||
|
||||
def run_task(task):
|
||||
loop.create_task(task)
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
|
||||
# detect ASGI to use as WebSocket as mixed protocol
|
||||
|
@ -435,7 +460,7 @@ class _WSGI:
|
|||
"REMOTE_HOST": "",
|
||||
"CONTENT_LENGTH": "0",
|
||||
"CONTENT_TYPE": "",
|
||||
'wsgi.input_terminated': True
|
||||
"wsgi.input_terminated": True,
|
||||
}
|
||||
)
|
||||
self.server.listen(port_or_options, handler)
|
||||
|
@ -454,7 +479,15 @@ class _WSGI:
|
|||
|
||||
# "Public" WSGI interface to allow easy forks/workers
|
||||
class WSGI:
|
||||
def __init__(self, app, options=None, websocket=None, websocket_options=None, task_factory_max_items=100_000, lifespan=False):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
options=None,
|
||||
websocket=None,
|
||||
websocket_options=None,
|
||||
task_factory_max_items=100_000,
|
||||
lifespan=False,
|
||||
):
|
||||
self.app = app
|
||||
self.options = options
|
||||
self.websocket = websocket
|
||||
|
@ -470,7 +503,11 @@ class WSGI:
|
|||
def run(self, workers=1):
|
||||
def run_app():
|
||||
server = _WSGI(
|
||||
self.app, self.options, self.websocket, self.websocket_options, self.task_factory_max_items
|
||||
self.app,
|
||||
self.options,
|
||||
self.websocket,
|
||||
self.websocket_options,
|
||||
self.task_factory_max_items,
|
||||
)
|
||||
if self.listen_options:
|
||||
(port_or_options, handler) = self.listen_options
|
||||
|
|
Ładowanie…
Reference in New Issue