emmett-framework / granian Goto Github PK
View Code? Open in Web Editor NEWA Rust HTTP server for Python applications
License: BSD 3-Clause "New" or "Revised" License
A Rust HTTP server for Python applications
License: BSD 3-Clause "New" or "Revised" License
As suggested by @Kludex
ASGI protocol might take advantage of granian capability of directly serve files from a path, like already implemented in RSGI proto.response_file
.
This would require to open up a PR to https://github.com/django/asgiref adding the new extra, which might look like the following:
"scope": {
...
"extensions": {
"http.response.file_path": {},
},
}
With the above extra definition, a new event might be produced by ASGI applications, with the following keys:
type
(Unicode string): "http.response.file_path"file_path
(Unicode string): The string representation of the file path to serve (platform specific)The ASGI application is still responsible to send the http.response.start
event with the relevant headers (eg: content-type
, content-length
, etc.)
First of all I would like to say this project is very cool, and I would definitely see the benefit of using this in an app i'm building. Problem is, besides the README, I don't really know what can I do to best implement this.
I would for sure be open to write the docs myself on how to use it. Do you have any plans on adding them in the future ? Would you be open to talk about it by email or something ?
Best,
bogdzn
I've tried to run https://github.com/benbusby/whoogle-search a WSGI application using granian --interface wsgi app
.
The logs show that wsgi.file_wrapper
is not supported:
[ERROR] Exception on /static/build/main.68be5054.css [GET]
Traceback (most recent call last):
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/app.py", line 2446, in wsgi_app
response = self.full_dispatch_request()
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/app.py", line 1951, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/app.py", line 1820, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/app.py", line 1949, in full_dispatch_request
rv = self.dispatch_request()
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/app.py", line 1935, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/helpers.py", line 1081, in send_static_file
return send_from_directory(
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/helpers.py", line 771, in send_from_directory
return send_file(filename, **options)
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/flask/helpers.py", line 640, in send_file
data = wrap_file(request.environ, file)
File "/home/alexandre/code/whoogle-search/venv/lib/python3.9/site-packages/werkzeug/wsgi.py", line 529, in wrap_file
return environ.get("wsgi.file_wrapper", FileWrapper)(file, buffer_size)
TypeError: 'NoneType' object is not callable
For reference: https://peps.python.org/pep-3333/#optional-platform-specific-file-handling
Some frameworks like FastAPI doesn't implement theirselves a reloader but rely on the server for that. Covering 1:1 portability requires this.
Might be related to #34
As suggested by @sansyrox
Apart from producing wheels, building also a Rust library would be beneficial for projects like https://github.com/sansyrox/robyn
Exposing Rust APIs would let other Rust projects to take advantage of granian
server interfaces to serve and interact with Python code.
This would probably require the re-design of server, protocols and types interfaces. A proper interface still need to be designed, probably starting from robyn needs.
Hi there! When I try to add Granian to a project, it fails.
/tmp
❯ mkdir testpoetry-granian
/tmp
❯ cd testpoetry-granian
/tmp/testpoetry-granian
❯ poetry init -q
/tmp/testpoetry-granian is 📦 v0.1.0 via 🐍 v3.11.0
❯ poetry add granian==0.3.0
Creating virtualenv testpoetry-granian in /private/tmp/testpoetry-granian/.venv
Updating dependencies
Resolving dependencies... (0.1s)
Writing lock file
Package operations: 8 installs, 0 updates, 0 removals
• Installing idna (3.4)
• Installing sniffio (1.3.0)
• Installing anyio (3.6.2)
• Installing click (8.1.3)
• Installing typer (0.7.0)
• Installing uvloop (0.17.0)
• Installing watchfiles (0.18.1)
• Installing granian (0.3.0): Failed
AssertionError
In /Users/lev/Library/Caches/pypoetry/artifacts/60/fe/00/2931d21a92fc2dc50a454589ca558160dde1d497d46471b44c75b3c653/granian-0.3.0-cp311-cp311-macosx_11_0_arm64.whl, granian/__init__.py is not mentioned in RECORD
at /opt/homebrew/Cellar/poetry/1.4.0/libexec/lib/python3.11/site-packages/installer/sources.py:158 in get_contents
154│ if item.filename[-1:] == "/": # looks like a directory
155│ continue
156│
157│ record = record_mapping.pop(item.filename, None)
→ 158│ assert record is not None, "In {}, {} is not mentioned in RECORD".format(
159│ self._zipfile.filename,
160│ item.filename,
161│ ) # should not happen for valid wheels
162│
/tmp/testpoetry-granian is 📦 v0.1.0 via 🐍 v3.11.2
❯
Here are steps that I took:
cd /tmp
mkdir testpoetry-granian
cd testpoetry-granian
poetry init -q
poetry add granian==0.3.0
I'm using ARM Mac. Python 3.11.2, Poetry 1.4.0. Doesn't work with Granian 0.2.0 either.
With Flask===2.2.2 Werkzeug==2.2.2
:
from flask import Flask, Response
app = Flask(__name__)
@app.route('/')
def hello_world():
resp = Response('Hello, World!')
resp.set_cookie('keyA', 'value1')
resp.set_cookie('keyB', 'value2')
return resp
$ granian --interface wsgi test_cookies.py
$ curl -vvv http://localhost:8000/
* Trying 127.0.0.1:10000...
* Connected to localhost (127.0.0.1) port 10000 (#0)
> GET / HTTP/1.1
> Host: localhost:10000
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< server: granian
< content-type: text/html; charset=utf-8
< content-length: 13
< set-cookie: keyB=value2; Path=/
< date: Wed, 18 Jan 2023 22:00:10 GMT
<
* Connection #0 to host localhost left intact
The first cookie keyA
was ignored.
This issue does not happen with other WSGI implementations or app.run()
.
werkzeug adds a new Set-Cookie
header for each cookie:
https://github.com/pallets/werkzeug/blob/529a26393272383754d3e2fe126d7854d8dd96f7/src/werkzeug/sansio/response.py#L228-L243
Hello!
Is support for alternative event loops planned?
I’d like to see how granian performs with or without uvloop against uvicorn with or without uvloop.
I'm running into the following runtime error when I try to run a WSGI server using granian with an Alpine Linux docker base image:
Traceback (most recent call last):
File "/usr/local/bin/granian", line 5, in <module>
from granian import cli
File "/usr/local/lib/python3.11/site-packages/granian/__init__.py", line 1, in <module>
from .server import Granian
File "/usr/local/lib/python3.11/site-packages/granian/server.py", line 12, in <module>
ImportError: Error relocating /usr/local/lib/python3.11/site-packages/granian/_granian.cpython-311-x86_64-linux-musl.so: __stack_chk_fail: initial-exec TLS resolves to dynamic definition in /usr/local/lib/python3.11/site-packages/granian/_granian.cpython-311-x86_64-linux-musl.so
Does anyone know how to resolve this?
Discussed in #23
Input in WSGI should be a stream object, as defined in PEP3333
Discussed in #17
Originally posted by cirospaciari December 1, 2022
If i call in granian
await send({
'type': 'websocket.send',
'bytes': None,
'text': 'something'
})
It will send an empty binary message instead of using the 'text', in uvicorn the 'text' is sent if bytes is None
In ASGI docs:
"Exactly one of bytes or text must be non-None. One or both keys may be present, however."
https://asgi.readthedocs.io/en/latest/specs/www.html#send-send-event
ASGI headers should respect Iterable[[byte string, byte string]]
type
Current broken implementation:
async def send(self, message):
handler = self._event_handlers[message["type"]]
handler(message)
This will always throw an exception, not calling handler, because all the handlers require two arguments (self and message).
So errored
always gets set, and lifespan.shutdown
never gets called.
Fix:
async def send(self, message):
handler = self._event_handlers[message["type"]]
handler(self, message)
Found because I wanted to try the example in the docs[1], but I never received a shutdown event.
Debugged via adding raise
after exception Exception
in handle
:
Traceback (most recent call last):
File "/home/johan/projects/tmp/granian/venv/lib/python3.10/site-packages/granian/asgi.py", line 25, in handle
await self.callable(
File "/home/johan/projects/tmp/granian/main.py", line 12, in app
await send({'type': 'lifespan.startup.complete', 'message': 'Complete'})
File "/home/johan/projects/tmp/granian/venv/lib/python3.10/site-packages/granian/asgi.py", line 102, in send
handler(message)
TypeError: LifespanProtocol._handle_startup_complete() missing 1 required positional argument: 'message'
[1] https://asgi.readthedocs.io/en/latest/specs/lifespan.html
Granian relies on quietly old typer and maybe time to have upgrade it with Granian 0.3.0?
Discussed in #17
Originally posted by cirospaciari December 1, 2022
Just testing the websockets ASGI protocol behavior, in uvicorn and others, the following code i used
async def app(scope, receive, send):
# handle non websocket
if scope['type'] == 'http':
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
})
await send({
'type': 'http.response.body',
'body': b'Connect via ws protocol!',
})
if scope['type'] != 'websocket':
return
protocols = scope.get('subprotocols', [])
scope = await receive()
# get connection
assert scope['type'] == 'websocket.connect'
# accept connection
await send({
'type': 'websocket.accept',
'subprotocol': protocols[0] if len(protocols) > 0 else None
})
# get data
while True:
scope = await receive()
type = scope['type']
# disconnected!
if type == 'websocket.disconnect':
print("disconnected!", scope)
break
# echo!
await send({
'type': 'websocket.send',
'bytes': scope.get('bytes', None),
'text': scope.get('text', '')
})
The order:
scope['type'] = websocket:
call await receive() to get the first websocket.connect
send websocket.accept to accept the connection
call await receive() to get websocket.receive or websocket.disconnect
send websocket.close if i want to end the connection
In granian ASGI:
async def app(scope, receive, send):
# handle non websocket
if scope['type'] == 'http':
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
})
await send({
'type': 'http.response.body',
'body': b'Connect via ws protocol!',
})
if scope['type'] != 'websocket':
return
protocols = scope.get('subprotocols', [])
# accept connection
await send({
'type': 'websocket.accept',
'subprotocol': protocols[0] if len(protocols) > 0 else None
})
scope = await receive()
assert scope['type'] == 'websocket.connect'
# get data
while True:
scope = await receive()
type = scope['type']
# disconnected!
if type == 'websocket.disconnect':
print("disconnected!", scope)
break
# echo!
await send({
'type': 'websocket.send',
'bytes': scope.get('bytes', None),
'text': scope.get('text', '')
})
The order:
scope['type'] = websocket:
send websocket.accept to accept the connection
call await receive() to get the first websocket.connect
call await receive() to get websocket.receive or websocket.disconnect
send websocket.close if i want to end the connection
Falcon source code for ASGI for reference:
async def _handle_websocket(self, ver, scope, receive, send):
first_event = await receive()
if first_event['type'] != EventType.WS_CONNECT:
# NOTE(kgriffs): The handshake was abandoned or this is a message
# we don't support, so bail out. This also fulfills the ASGI
# spec requirement to only process the request after
# receiving and verifying the first event.
await send({'type': EventType.WS_CLOSE, 'code': WSCloseCode.SERVER_ERROR})
return
https://github.com/falconry/falcon/blob/master/falcon/asgi/app.py Line 970
ASGI spec about websocket.connect
"This message must be responded to with either an Accept message or a Close message before the socket will pass websocket.receive messages. The protocol server must send this message during the handshake phase of the WebSocket and not complete the handshake until it gets a reply, returning HTTP status code 403 if the connection is denied."
https://asgi.readthedocs.io/en/latest/specs/www.html#connect-receive-event
One of the common problems with Django apps is memory leaks. The most common suggestion is to allow the webserver to cycle processes after serving some fixed number of requests. This bypasses memory issues by just returning used up memory and start fresh.
Perhaps you can consider supporting something similar to gunicorn early in the design.
gunicorn --max-requests 1000 --max-requests-jitter 50 app.wsgi
This is also supported in uwsgi, one of the popular alternatives to gunicorn.
Similar issues are solved in celery worker_max_tasks_per_child
since i think it's fairly hard to track down and fix memory issues in larger Python projects with hundreds of dependencies.
This can be done with the existing suite adding a lua script for wrk
This would avoid to load in memory the whole response body before actual send.
Related to #27
Graninan commandline: granian --interface asgi --http 2 main:app
OS: linux 6.2.8
granian 0.3.1
fastapi 0.92.0
curl 8.0.1
And how I tested it:
curl --http2-prior-knowledge -vvv http:/127.0.0.1:8000
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
* h2h3 [:method: GET]
* h2h3 [:path: /]
* h2h3 [:scheme: http]
* h2h3 [:authority: 127.0.0.1:8000]
* h2h3 [user-agent: curl/8.0.1]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x5650650f5ea0)
> GET / HTTP/2
> Host: 127.0.0.1:8000
> user-agent: curl/8.0.1
> accept: */*
>
< HTTP/2 404
< server: granian
< content-length: 22
< content-type: application/json
< date: Sat, 25 Mar 2023 13:00:30 GMT
<
* Connection #0 to host 127.0.0.1 left intact
"Hello"
curl --http2 -vvv http://127.0.0.1:8000
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/8.0.1
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA
>
* Received HTTP/0.9 when not allowed
* Closing connection 0
curl: (1) Received HTTP/0.9 when not allowed
##The bug is reproced not only with curl but in chromium, too.
Originally posted by cirospaciari December 1, 2022
Just testing the websockets ASGI protocol behavior, in uvicorn and other the following code i used
async def app(scope, receive, send):
# handle non websocket
if scope['type'] == 'http':
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
})
await send({
'type': 'http.response.body',
'body': b'Connect via ws protocol!',
})
if scope['type'] != 'websocket':
return
protocols = scope.get('subprotocols', [])
scope = await receive()
# get connection
assert scope['type'] == 'websocket.connect'
# accept connection
await send({
'type': 'websocket.accept',
'subprotocol': protocols[0] if len(protocols) > 0 else None
})
# get data
while True:
scope = await receive()
type = scope['type']
# disconnected!
if type == 'websocket.disconnect':
print("disconnected!", scope)
break
# echo!
await send({
'type': 'websocket.send',
'bytes': scope.get('bytes', None),
'text': scope.get('text', '')
})
The order:
scope['type'] = websocket:
call await receive() to get the first websocket.connect
send websocket.accept to accept the connection
call await receive() to get websocket.receive or websocket.disconnect
send websocket.close if i want to end the connection
In granian ASGI:
async def app(scope, receive, send):
# handle non websocket
if scope['type'] == 'http':
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
})
await send({
'type': 'http.response.body',
'body': b'Connect via ws protocol!',
})
if scope['type'] != 'websocket':
return
protocols = scope.get('subprotocols', [])
# accept connection
await send({
'type': 'websocket.accept',
'subprotocol': protocols[0] if len(protocols) > 0 else None
})
scope = await receive()
assert scope['type'] == 'websocket.connect'
# get data
while True:
scope = await receive()
type = scope['type']
# disconnected!
if type == 'websocket.disconnect':
print("disconnected!", scope)
break
# echo!
await send({
'type': 'websocket.send',
'bytes': scope.get('bytes', None),
'text': scope.get('text', '')
})
The order:
scope['type'] = websocket:
send websocket.accept to accept the connection
call await receive() to get the first websocket.connect
call await receive() to get websocket.receive or websocket.disconnect
send websocket.close if i want to end the connection
Falcon source code for ASGI for reference:
async def _handle_websocket(self, ver, scope, receive, send):
first_event = await receive()
if first_event['type'] != EventType.WS_CONNECT:
# NOTE(kgriffs): The handshake was abandoned or this is a message
# we don't support, so bail out. This also fulfills the ASGI
# spec requirement to only process the request after
# receiving and verifying the first event.
await send({'type': EventType.WS_CLOSE, 'code': WSCloseCode.SERVER_ERROR})
return
https://github.com/falconry/falcon/blob/master/falcon/asgi/app.py Line 970
ASGI spec about websocket.connect
"This message must be responded to with either an Accept message or a Close message before the socket will pass websocket.receive messages. The protocol server must send this message during the handshake phase of the WebSocket and not complete the handshake until it gets a reply, returning HTTP status code 403 if the connection is denied."
https://asgi.readthedocs.io/en/latest/specs/www.html#connect-receive-event
Another thing is if i call in granian
await send({
'type': 'websocket.send',
'bytes': None,
'text': 'something'
})
It will send an empty binary message instead of using the 'text', in uvicorn the 'text' is sent if bytes is None
In ASGI docs:
"Exactly one of bytes or text must be non-None. One or both keys may be present, however."
https://asgi.readthedocs.io/en/latest/specs/www.html#send-send-event
I wanted to give granian a try but WSGI support seems to be somewhat out of spec. Eg try:
class Response:
def __init__(self, parts):
self.parts = parts
def __iter__(self):
return self
def __next__(self):
try:
return self.parts.pop(0)
except Exception:
raise StopIteration
def close(self):
print("CLOSE CALLED")
def app(env, start_response):
start_response('200 OK', [('Content-Type','text/html')])
return Response([b"Hello", b" ", b"World"])
When running this, "Hello World" is returned properly but .close()
is not called, see https://peps.python.org/pep-3333/#specification-details
$ granian
Illegal instruction
$ uname -a
Linux pizero 6.1.31+ #1654 Tue May 30 17:08:26 BST 2023 armv6l GNU/Linux
$ dpkg --print-architecture
armhf
$ sudo cat /proc/cpuinfo
processor : 0
model name : ARMv6-compatible processor rev 7 (v6l)
BogoMIPS : 997.08
Features : half thumb fastmult vfp edsp java tls
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xb76
CPU revision : 7
Hardware : BCM2835
Revision : 9000c1
Serial : 0000000020b13298
Model : Raspberry Pi Zero W Rev 1.1
usr/local/lib/python3.9/dist-packages/granian $ objdump -f _granian.cpython-39-arm-linux-gnueabihf.so
_granian.cpython-39-arm-linux-gnueabihf.so: file format elf32-littlearm
architecture: armv7, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x0000e700
Both Raspbian and Debian pride themselves in suppporting the "armhf" architecture. Of course, they mean two different things !
Raspbian "armhf": ARMv6 + VFPv2
Debian "armhf": ARMv7
Great project, was trying to run with django got some error
myproj_local_django | Running migrations:
myproj_local_django | No migrations to apply.
myproj_local_django | [INFO] Starting granian
myproj_local_django | [INFO] Listening at: 0.0.0.0:8000
myproj_local_django | [INFO] Spawning worker-1 with pid: 9
myproj_local_django | INFO 2023-05-27 15:11:17,234 serve 9 139912409515840 Started worker-1
myproj_local_django | INFO 2023-05-27 15:11:17,234 serve 9 139912409515840 Started worker-1 runtime-1
myproj_local_django | WARNING 2023-05-27 15:11:28,481 callbacks 9 139912409515840 Application callable raised an exception
myproj_local_django | Exception ignored in: <coroutine object application at 0x7f3fe1deba60>
myproj_local_django | Traceback (most recent call last):
myproj_local_django | File "/app/config/asgi.py", line 36, in application
myproj_local_django | await django_application(scope, receive, send)
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/django/core/handlers/asgi.py", line 154, in __call__
myproj_local_django | async with ThreadSensitiveContext():
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 127, in __aexit__
myproj_local_django | SyncToAsync.thread_sensitive_context.reset(self.token)
myproj_local_django | ValueError: <Token var=<ContextVar name='thread_sensitive_context' at 0x7f3fe3d790d0> at 0x7f3fe1c8e680> was created in a different Context
myproj_local_django | ERROR 2023-05-27 15:11:28,530 log 9 139912330168064 Internal Server Error: /
myproj_local_django | Traceback (most recent call last):
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 534, in thread_handler
myproj_local_django | raise exc_info[1]
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 43, in inner
myproj_local_django | response = await get_response(request)
myproj_local_django | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/django/utils/deprecation.py", line 148, in __acall__
myproj_local_django | response = await sync_to_async(
myproj_local_django | ^^^^^^^^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 479, in __call__
myproj_local_django | ret: _R = await loop.run_in_executor(
myproj_local_django | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 58, in run
myproj_local_django | result = self.fn(*self.args, **self.kwargs)
myproj_local_django | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 538, in thread_handler
myproj_local_django | return func(*args, **kwargs)
myproj_local_django | ^^^^^^^^^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/django/middleware/locale.py", line 35, in process_request
myproj_local_django | translation.activate(language)
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/django/utils/translation/__init__.py", line 181, in activate
myproj_local_django | return _trans.activate(language)
myproj_local_django | ^^^^^^^^^^^^^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/django/utils/translation/trans_real.py", line 303, in activate
myproj_local_django | _active.value = translation(language)
myproj_local_django | ^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/local.py", line 111, in __setattr__
myproj_local_django | storage = self._get_storage()
myproj_local_django | ^^^^^^^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/local.py", line 83, in _get_storage
myproj_local_django | setattr(context_obj, self._attr_name, {})
myproj_local_django | AttributeError: 'NoneType' object has no attribute '_asgiref_local_impl_139912375751504_IvidgRsU'
myproj_local_django | WARNING 2023-05-27 15:11:28,552 callbacks 9 139912409515840 Application callable raised an exception
myproj_local_django | Exception ignored in: <coroutine object application at 0x7f3fe1ca8040>
myproj_local_django | Traceback (most recent call last):
myproj_local_django | File "/app/config/asgi.py", line 36, in application
myproj_local_django | ERROR 2023-05-27 15:11:28,592 log 9 139912330168064 Internal Server Error: /favicon.ico
myproj_local_django | Traceback (most recent call last):
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 534, in thread_handler
myproj_local_django | raise exc_info[1]
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 43, in inner
myproj_local_django | response = await get_response(request)
myproj_local_django | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
myproj_local_django | ^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/local.py", line 111, in __setattr__
myproj_local_django | storage = self._get_storage()
myproj_local_django | ^^^^^^^^^^^^^^^^^^^
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/local.py", line 83, in _get_storage
myproj_local_django | setattr(context_obj, self._attr_name, {})
myproj_local_django | AttributeError: 'NoneType' object has no attribute '_asgiref_local_impl_139912375751504_IvidgRsU'
myproj_local_django | await django_application(scope, receive, send)
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/django/core/handlers/asgi.py", line 154, in __call__
myproj_local_django | async with ThreadSensitiveContext():
myproj_local_django | File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 127, in __aexit__
myproj_local_django | SyncToAsync.thread_sensitive_context.reset(self.token)
myproj_local_django | ValueError: <Token var=<ContextVar name='thread_sensitive_context' at 0x7f3fe3d790d0> at 0x7f3fe0c567c0> was created in a different Context
Here's how I run granian
exec granian --interface asgi config.asgi:application --host 0.0.0.0
and here's my asgi.py
"""
ASGI config for Myproj project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/dev/howto/deployment/asgi/
"""
import os
import sys
from pathlib import Path
from django.core.asgi import get_asgi_application
# This allows easy placement of apps within the interior
# myproj directory.
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
sys.path.append(str(BASE_DIR / "myproj"))
# If DJANGO_SETTINGS_MODULE is unset, default to the local settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
# This application object is used by any ASGI server configured to use this file.
django_application = get_asgi_application()
# Apply ASGI middleware here.
# from helloworld.asgi import HelloWorldApplication
# application = HelloWorldApplication(application)
# Import websocket application here, so apps from django_application are loaded first
from config.websocket import websocket_application # noqa isort:skip
async def application(scope, receive, send):
if scope["type"] == "http":
await django_application(scope, receive, send)
elif scope["type"] == "websocket":
await websocket_application(scope, receive, send)
else:
raise NotImplementedError(f"Unknown scope type {scope['type']}")
Runs perfectly fine with uvicorn, daphne or nginx unit. Also runs fine with granian's wsgi interface.
This need a new tool in suite, as wrk
won't support HTTP/2
Better to do this after #25
Tracking ref: hyperium/hyper#3088
Causing the stack trace reported in #40
Hi there! Congrats for this great project, looking forward to see where it'll go!
I have some difficulties trying to understand the need for a different specification (RSGI) in addition to the two widely used AGSI and WSGI. Would you mind adding a "motivation" section to the spec, emphasizing this need, what ASGI/WSGI lacks, why RSGI needs to fill this hole, how it's better, when to use which, etc?
Hi, im getting this issue in version 0.4.2.
asynclib_name = sniffio.current_async_library()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
raise AsyncLibraryNotFoundError(
sniffio._impl.AsyncLibraryNotFoundError: unknown async library, or not in async context
Any ideas?
This is a very exciting project - thanks for releasing this!
I tried to get it working with my https://datasette.io/ ASGI app and ran into this error:
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 121, in start
self._popen = self._Popen(self)
^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/context.py", line 288, in _Popen
return Popen(process_obj)
^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/popen_spawn_posix.py", line 32, in __init__
super().__init__(process_obj)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/popen_fork.py", line 19, in __init__
self._launch(process_obj)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/popen_spawn_posix.py", line 47, in _launch
reduction.dump(process_obj, fp)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/reduction.py", line 60, in dump
ForkingPickler(file, protocol).dump(obj)
AttributeError: Can't pickle local object 'asgi_csrf_decorator.<locals>._asgi_csrf_decorator.<locals>.app_wrapped_with_csrf'
Here's the script I wrote to replicate the problem, saved as serve_datasette_with_granian.py
:
from granian.server import Granian
from granian.constants import HTTPModes, Interfaces, Loops, ThreadModes
from granian.log import LogLevels
from datasette.app import Datasette
def serve_with_granian():
ds = Datasette(memory=True)
app = ds.app()
Granian(
app,
address="127.0.0.1",
port=8002,
interface=Interfaces.ASGI,
workers=1,
threads=1,
pthreads=1,
threading_mode=ThreadModes.workers.value,
loop=Loops.auto.value,
http=HTTPModes.auto.value,
websockets=True,
backlog=1024,
log_level=LogLevels.info.value,
ssl_cert=None,
ssl_key=None,
).serve()
if __name__ == "__main__":
serve_with_granian()
Run it like this to see the error (run pip install datasette
first):
python serve_datasette_with_granian.py
Are there changes I can make to Datasette to get this to work, or is this something that illustrates a bug in Granian?
Relevant Datasette code is here: https://github.com/simonw/datasette/blob/6a352e99ab988dbf8fd22a100049caa6ad33f1ec/datasette/app.py#L1429-L1454
It's applying my asgi-csrf
ASGI middleware from https://github.com/simonw/asgi-csrf
Hi all,
Firstly, great project! I am looking forward to using this in one of my projects.
One thing that would be really good to have is the ability to override the logging config like gunicorn allows you to do. In gunicorn, you can provide a logconfig_dict
which lets you specific file path, date format, date formats etc. It would be really good to have this here as well.
I don't think it would be very hard to implement it. I see the config is hard coded here
Line 26 in d69da15
Great project!Just out of curious, is there any ASGI Extension support in plan?
getting this error when trying to run the application
2023-05-25 15:16:07,111] INFO root Starting granian
[2023-05-25 15:16:07,112] INFO root Listening at: 0.0.0.0:8080
Traceback (most recent call last):
File "/Users/myuser/project_folder/project_subfolder/app/server.py", line 48, in
run_application()
File "/Users/myuser/project_folder/project_subfolder/app/server.py", line 28, in run_application
granian.serve()
File "/Users/myuser/Library/Caches/pypoetry/virtualenvs/sigrhe-bmt3Z-Ej-py3.11/lib/python3.11/site-packages/granian/server.py", line 368, in serve
serve_method(spawn_target, target_loader)
File "/Users/myuser/Library/Caches/pypoetry/virtualenvs/sigrhe-bmt3Z-Ej-py3.11/lib/python3.11/site-packages/granian/server.py", line 335, in _serve_with_reloader
sock = self.startup(spawn_target, target_loader)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/myuser/Library/Caches/pypoetry/virtualenvs/sigrhe-bmt3Z-Ej-py3.11/lib/python3.11/site-packages/granian/server.py", line 321, in startup
self._spawn_workers(sock, spawn_target, target_loader)
File "/Users/myuser/Library/Caches/pypoetry/virtualenvs/sigrhe-bmt3Z-Ej-py3.11/lib/python3.11/site-packages/granian/server.py", line 300, in _spawn_workers
proc.start()
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 121, in start
self._popen = self._Popen(self)
^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/context.py", line 288, in _Popen
return Popen(process_obj)
^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/popen_spawn_posix.py", line 32, in init
super().init(process_obj)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/popen_fork.py", line 19, in init
self._launch(process_obj)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/popen_spawn_posix.py", line 47, in _launch
reduction.dump(process_obj, fp)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/reduction.py", line 60, in dump
ForkingPickler(file, protocol).dump(obj)
TypeError: cannot pickle '_io.TextIOWrapper' object
Involved steps:
Related to #27
This will need a custom implementation into benchmarks suite
Can be done with the existing suite, just need a static file in benchmarks folder
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.