Coder Social home page Coder Social logo

emmett-framework / granian Goto Github PK

View Code? Open in Web Editor NEW
2.4K 22.0 75.0 781 KB

A Rust HTTP server for Python applications

License: BSD 3-Clause "New" or "Revised" License

Python 32.63% Rust 66.60% Makefile 0.41% Smarty 0.37%
asgi asyncio http http-server python rust rsgi wsgi

granian's Introduction

Granian

A Rust HTTP server for Python applications.

Rationale

The main reasons behind Granian design are:

  • Have a single, correct HTTP implementation, supporting versions 1, 2 (and eventually 3)
  • Provide a single package for several platforms
  • Avoid the usual Gunicorn + uvicorn + http-tools dependency composition on unix systems
  • Provide stable performance when compared to existing alternatives

Features

  • Supports ASGI/3, RSGI and WSGI interface applications
  • Implements HTTP/1 and HTTP/2 protocols
  • Supports HTTPS
  • Supports Websockets

Quickstart

You can install Granian using pip:

$ pip install granian

Create an ASGI application in your main.py:

async def app(scope, receive, send):
    assert 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'Hello, world!',
    })

and serve it:

$ granian --interface asgi main:app

You can also create an app using the RSGI specification:

async def app(scope, proto):
    assert scope.proto == 'http'

    proto.response_str(
        status=200,
        headers=[
            ('content-type', 'text/plain')
        ],
        body="Hello, world!"
    )

and serve it using:

$ granian --interface rsgi main:app

Options

You can check all the options provided by Granian with the --help command:

$ granian --help
Usage: granian [OPTIONS] APP

  APP  Application target to serve.  [required]

Options:
  --host TEXT                     Host address to bind to  [env var:
                                  GRANIAN_HOST; default: (127.0.0.1)]
  --port INTEGER                  Port to bind to.  [env var: GRANIAN_PORT;
                                  default: 8000]
  --interface [asgi|asginl|rsgi|wsgi]
                                  Application interface type  [env var:
                                  GRANIAN_INTERFACE; default: (rsgi)]
  --http [auto|1|2]               HTTP version  [env var: GRANIAN_HTTP;
                                  default: (auto)]
  --ws / --no-ws                  Enable websockets handling  [env var:
                                  GRANIAN_WEBSOCKETS; default: (enabled)]
  --workers INTEGER RANGE         Number of worker processes  [env var:
                                  GRANIAN_WORKERS; default: 1; x>=1]
  --threads INTEGER RANGE         Number of threads (per worker)  [env var:
                                  GRANIAN_THREADS; default: 1; x>=1]
  --blocking-threads INTEGER RANGE
                                  Number of blocking threads (per worker)
                                  [env var: GRANIAN_BLOCKING_THREADS; x>=1]
  --threading-mode [runtime|workers]
                                  Threading mode to use  [env var:
                                  GRANIAN_THREADING_MODE; default: (workers)]
  --loop [auto|asyncio|uvloop]    Event loop implementation  [env var:
                                  GRANIAN_LOOP; default: (auto)]
  --opt / --no-opt                Enable loop optimizations  [env var:
                                  GRANIAN_LOOP_OPT; default: (disabled)]
  --backlog INTEGER RANGE         Maximum number of connections to hold in
                                  backlog (globally)  [env var:
                                  GRANIAN_BACKLOG; default: 1024; x>=128]
  --backpressure INTEGER RANGE    Maximum number of requests to process
                                  concurrently (per worker)  [env var:
                                  GRANIAN_BACKPRESSURE; default:
                                  (backlog/workers); x>=1]
  --http1-buffer-size INTEGER RANGE
                                  Set the maximum buffer size for HTTP/1
                                  connections  [env var:
                                  GRANIAN_HTTP1_BUFFER_SIZE; default: 417792;
                                  x>=8192]
  --http1-keep-alive / --no-http1-keep-alive
                                  Enables or disables HTTP/1 keep-alive  [env
                                  var: GRANIAN_HTTP1_KEEP_ALIVE; default:
                                  (enabled)]
  --http1-pipeline-flush / --no-http1-pipeline-flush
                                  Aggregates HTTP/1 flushes to better support
                                  pipelined responses (experimental)  [env
                                  var: GRANIAN_HTTP1_PIPELINE_FLUSH; default:
                                  (disabled)]
  --http2-adaptive-window / --no-http2-adaptive-window
                                  Sets whether to use an adaptive flow control
                                  for HTTP2  [env var:
                                  GRANIAN_HTTP2_ADAPTIVE_WINDOW; default:
                                  (disabled)]
  --http2-initial-connection-window-size INTEGER
                                  Sets the max connection-level flow control
                                  for HTTP2  [env var: GRANIAN_HTTP2_INITIAL_C
                                  ONNECTION_WINDOW_SIZE; default: 1048576]
  --http2-initial-stream-window-size INTEGER
                                  Sets the `SETTINGS_INITIAL_WINDOW_SIZE`
                                  option for HTTP2 stream-level flow control
                                  [env var:
                                  GRANIAN_HTTP2_INITIAL_STREAM_WINDOW_SIZE;
                                  default: 1048576]
  --http2-keep-alive-interval INTEGER
                                  Sets an interval for HTTP2 Ping frames
                                  should be sent to keep a connection alive
                                  [env var: GRANIAN_HTTP2_KEEP_ALIVE_INTERVAL]
  --http2-keep-alive-timeout INTEGER
                                  Sets a timeout for receiving an
                                  acknowledgement of the HTTP2 keep-alive ping
                                  [env var: GRANIAN_HTTP2_KEEP_ALIVE_TIMEOUT;
                                  default: 20]
  --http2-max-concurrent-streams INTEGER
                                  Sets the SETTINGS_MAX_CONCURRENT_STREAMS
                                  option for HTTP2 connections  [env var:
                                  GRANIAN_HTTP2_MAX_CONCURRENT_STREAMS;
                                  default: 200]
  --http2-max-frame-size INTEGER  Sets the maximum frame size to use for HTTP2
                                  [env var: GRANIAN_HTTP2_MAX_FRAME_SIZE;
                                  default: 16384]
  --http2-max-headers-size INTEGER
                                  Sets the max size of received header frames
                                  [env var: GRANIAN_HTTP2_MAX_HEADERS_SIZE;
                                  default: 16777216]
  --http2-max-send-buffer-size INTEGER
                                  Set the maximum write buffer size for each
                                  HTTP/2 stream  [env var:
                                  GRANIAN_HTTP2_MAX_SEND_BUFFER_SIZE; default:
                                  409600]
  --log / --no-log                Enable logging  [env var:
                                  GRANIAN_LOG_ENABLED; default: (enabled)]
  --log-level [critical|error|warning|warn|info|debug]
                                  Log level  [env var: GRANIAN_LOG_LEVEL;
                                  default: (info)]
  --log-config FILE               Logging configuration file (json)  [env var:
                                  GRANIAN_LOG_CONFIG]
  --access-log / --no-access-log  Enable access log  [env var:
                                  GRANIAN_LOG_ACCESS_ENABLED; default:
                                  (disabled)]
  --access-log-fmt TEXT           Access log format  [env var:
                                  GRANIAN_LOG_ACCESS_FMT]
  --ssl-keyfile FILE              SSL key file  [env var: GRANIAN_SSL_KEYFILE]
  --ssl-certificate FILE          SSL certificate file  [env var:
                                  GRANIAN_SSL_CERTIFICATE]
  --url-path-prefix TEXT          URL path prefix the app is mounted on  [env
                                  var: GRANIAN_URL_PATH_PREFIX]
  --respawn-failed-workers / --no-respawn-failed-workers
                                  Enable workers respawn on unexpected exit
                                  [env var: GRANIAN_RESPAWN_FAILED_WORKERS;
                                  default: (disabled)]
  --respawn-interval FLOAT        The number of seconds to sleep between
                                  workers respawn  [env var:
                                  GRANIAN_RESPAWN_INTERVAL; default: 3.5]
  --reload / --no-reload          Enable auto reload on application's files
                                  changes (requires granian[reload] extra)
                                  [env var: GRANIAN_RELOAD; default:
                                  (disabled)]
  --process-name TEXT             Set a custom name for processes (requires
                                  granian[pname] extra)  [env var:
                                  GRANIAN_PROCESS_NAME]
  --version                       Show the version and exit.
  --help                          Show this message and exit.

Access log format

The access log format can be configured by specifying the atoms (see below) to include in a specific format. By default Granian will use [%(time)s] %(addr)s - "%(method)s %(path)s %(protocol)s" %(status)d %(dt_ms).3f as the format.

Access log atoms

The following atoms are available for use:

identifier description
addr Client remote address
time Datetime of the request
dt_ms Request duration in ms
status HTTP response status
path Request path (without query string)
query_string Request query string
method Request HTTP method
scheme Request scheme
protocol HTTP protocol version

Processes and threads

Granian offers different options to configure the number of processes and threads to be run, in particular:

  • workers: the total number of processes holding a dedicated Python interpreter that will run the application
  • threads: the number of Rust threads per worker that will perform network I/O
  • blocking threads: the number of Rust threads per worker involved in blocking operations. The main role of these threads is to deal with blocking I/O – like opening files – but on synchronous protocols like WSGI these threads will also be responsible of interacting with the application code.

In general, Granian will try its best to automatically pick proper values for the threading configuration, leaving to you the responsibility to choose the number of workers you need.
There is no golden rule here, as these numbers will vastly depend both on your application behavior and the deployment target, but we can list some suggestions:

  • matching the amount of CPU cores for the workers is generally the best starting point; on containerized environments like docker or k8s is best to have 1 worker per container though and scale your containers using the relevant orchestrator;
  • the default number of threads is fine for the vast majority of applications out there; you might want to increase this number for applications dealing with several concurrently opened websockets;
  • the default number of blocking threads should work properly with the majority of applications; in synchronous protocols like WSGI this will also impact the number of concurrent requests you can handle, but you should use the backpressure configuration parameter to control it and set a lower number of blocking threads only if your application has a very low (1ms order) average response time;

Also, you should generally avoid to configure workers and threads based on numbers of other servers, as Granian architecture is quite different from projects like Gunicorn or Uvicorn.

Threading mode

Granian offers two different threading paradigms, due to the fact the inner Rust runtime can be multi-threaded – in opposition to what happens in Python event-loop which can only run as a single thread.

Given you specify N threads with the relevant option, in workers threading mode Granian will spawn N single-threaded Rust runtimes, while in runtime threading mode Granian will spawn a single multi-threaded runtime with N threads.

Benchmarks suggests workers mode to be more efficient with a small amount of processes, while runtime mode seems to scale more efficiently where you have a large number of CPUs. Real performance will though depend on specific application code, and thus your mileage might vary.

Event loop optimizations

With the --opt option Granian will use custom task handlers for Python coroutines and awaitables to improve Python code execution. Due to the nature of such handlers some libraries and specific application code relying on asyncio internals might not work.

You might test the effect such optimizations cause over your application and decide whether to enable 'em or leave 'em disabled (as per default).

Project status

Granian is currently under active development.

Granian is compatible with Python 3.8 and above versions.

License

Granian is released under the BSD License.

granian's People

Contributors

2-5 avatar aeron avatar bluetech avatar cirospaciari avatar dekkers avatar dependabot[bot] avatar elbaro avatar eltociear avatar gabrielmbmb avatar gi0baro avatar github-actions[bot] avatar kianmeng avatar matthiask avatar rafaelwo avatar shulcsm avatar stumpylog avatar thejecksman avatar timkofu avatar wakayser 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

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

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

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?

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

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.

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.

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

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

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

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

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

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?

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.

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

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?

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

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.

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

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.

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.