Giter VIP home page Giter VIP logo

granian's People

Contributors

aeron avatar bluetech avatar cirospaciari avatar dekkers avatar dependabot[bot] avatar eltociear avatar gabrielmbmb avatar gi0baro avatar kianmeng avatar matthiask avatar rafaelwo avatar shulcsm avatar stumpylog avatar timkofu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

granian's Issues

WSGI support is not spec conformant

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

Issue with using on par with poetry

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.

HTTP/1.1 Upgrade to HTTP/2 not working

I have bootrapped simple app with FastAPI and tried to lauch it on ASGI with granian, and then found that HTTP/1.1 Upgrade to HTTP/2 not working.

Graninan commandline: granian --interface asgi --http 2 main:app

Environment

 OS: linux 6.2.8
 granian 0.3.1
 fastapi 0.92.0
 curl 8.0.1 

And how I tested it:

  1. http2 without upgrade: 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" 
  1. http2 with upgrade from http1: 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.

Documentation

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

Add support for hot-reload

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

Provide a way to override logging config

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

LOGGING_CONFIG = {
, if we can provide an option to read this - and other command line arguments from a json/toml file, that would be enough.

AsyncLibraryNotFoundError

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?

Does not work on Raspberry Pi Zero

$ 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

https://raspberrypi.stackexchange.com/questions/87392/pi1-armv6-how-to-disable-armhf-packages/87403#87403:

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

[BUG] ASGI websockets behavior websocket.connect and websocket.accept

Discussed in #17

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

Alternative event loops

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.

Ability to serve an ASGI app object directly, rather than passing a module string

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

Funding

  • You can sponsor this specific effort via a Polar.sh pledge below
  • We receive the pledge once the issue is completed & verified
Fund with Polar

Flask: calls to response.set_cookie are ignored except the last one

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

Implement ASGI extra `http.response.pathsend`

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.)

Expose Rust interfaces

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.

Support `max-requests` and `max-requests-jitter` config parameters

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.

References

Funding

  • You can sponsor this specific effort via a Polar.sh pledge below
  • We receive the pledge once the issue is completed & verified
Fund with Polar

Avoid `pyo3-asyncio` fork

Involved steps:

  • consolidate changes to make a PR into the upstream project
  • requires #32
  • open up a discussion in the upstream project

RSGI Motivation

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?

ASGI websockets wrong behavior for websocket.connect and websocket.accept

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

LifespanProtocol.send is broken

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

ImportError when run on Alpine Linux

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?

cannot pickle _io.TextiOWrapper

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

Add support for wsgi.file_wrapper

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

Interface asgi error with django

Great project, was trying to run with django got some error

logs
    
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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.