Skip to main content

Simple and fast WSGI server in Go

Project description

wsgo

A simple and fast Python WSGI application server written in Go, with sensible defaults and minimal configuration.

Installation

Binary packages are available via PyPI for Linux x86_64/arm64, install with:

pip install wsgo

Quickstart

  1. Create a wsgi_app.py file containing:
def application(env, start_response):
    start_response('200 OK', [
        ('Content-Type','text/plain'),
    ])
    return [b'Hello world!']
  1. Start wsgo with:

wsgo --module wsgi_app --http-socket 127.0.0.1:8000

  1. Go to http://127.0.0.1:8000 in your web browser.

Usage

wsgo 
  --workers 8
  --processes 2
  --module wsgi_app 
  --static-map /=webroot 
  --http-socket 0.0.0.0:8000
  --request-timeout 300

Features

  • Simple configuration with sensible defaults
  • Single executable (although requires the Python3 it is linked against to be installed)
  • Multithreaded, with a multi-process mode
  • Built-in header-controlled response cache
  • Static file serving
  • Request prioritisation (by URL prefix, request properties, and connection counts)
  • Cron-like system for running background tasks
  • Request parking mechanism to support long-polling
  • Blocking mechanism for DoS mitigation

Building from source

If you have Docker installed, you can run ./build.sh to produce Go executables and Python wheels for all currently supported versions of Python, in the dist/ folder.

Then either run pip install <dist/file.whl> to install wsgo in your bin folder, or manually retrieve the appropriate dist/wsgo-* executable and include it with your app.

Caveats

  • Uses Cgo, with lots of unsafeness
  • Poorly tested (only with Django on Ubuntu/Debian x86 platforms so far)
  • Opinionated, with some hardcoded behaviour

Known issues

  • The wsgi.input file-like object only implements the read(n) and readline() methods (this is enough for Django).
  • It can't yet be built via the regular python setup.py compile mechanism.

Documentation

Multithreading and multi-process

 --workers <number of threads per process>     (default 16)
 --processes <number of processes>             (default 1)

By default several worker threads will be started, so that multiple responses can be handled in parallel. This requires that your app is thread-safe - if it isn't then you can limit the thread count with --workers 1.

Due to the Python Global Interpreter Lock (GIL), only one thread can run at a time, so a single process cannot use more than one core. If you start wsgo with the --processes <n> option, then n processes will be started, each with a full set of threads. The listening socket has SO_REUSEPORT set so that multiple processes can bind to the same address and will share the incoming request stream.

The most appropriate number of threads or processes will depend on your application[^1].

Timeouts

 --request-timeout <timeout in seconds>       (default 60)

There is a single configurable request timeout. If a worker is processing a request for longer than this, then a wsgo.RequestTimeoutException will be raised asynchronously in the thread to interrupt it.

Interrupting a running worker can cause problems in some code,[^2] resulting in the worker getting 'stuck'. If all of the workers get into a 'stuck' state simultaneously, the process will exit and be restarted. Note that if four or more requests from the same IP are 'stuck', the server may still be responsive to others, but that IP won't be able to make any further requests due to the priority-based IP limiting.

The actual http server has longer hardcoded timeouts (2 seconds to read the request header, 600 seconds to read the PUT/PATCH/POST body, 3600 seconds to write the response body, and 60 seconds max idle for a keep-alive). This is due to a Go limitation, where these can't be altered per-request, and so need to be large enough to accommodate the slowest uploads and downloads. However Go's coroutine mechanism means that a large number of lingering requests is not an issue, as long as the Python threads themselves are not overloaded.

Static file serving

 --static-map <path prefix>=<local path>

eg:
 --static-map /=webroot
      # eg: requests to /favicon.ico will map to ./webroot/favicon.ico
 --static-map /media=storage_dir/media_files
      # eg: requests to /media/images/file1.jpg will map to ./storage_dir/media_files/images/file1.jpg

Static file serving is enabled by specifying one or more static mappings. If a request matches a static mapping path prefix, then the specified local path will be checked for a matching file (after stripping the mapping path prefix from the request), and if found it will be sent as the response.

Static file serving happens before WSGI request handling, so if a matching static file is found, it will be returned as the response and processing will finish without calling the WSGI handler. If no matching file was found (even if a mapping prefix matched), the request will be passed on to the WSGI handler.

Static files (after relative paths are resolved and symlinks are followed) must reside within the local path specified, which prevents escapes such as requests to /media/../../../../etc/passwd. You therefore cannot serve files which are symlinked to outside of the local path.

You can place gzipped versions of static files adjacent to the originals, with the suffix .gz, for example:

./webroot/static/styles.css
./webroot/static/styles.css.gz

Any request to /static/styles.css with a Accept-Encoding: header including gzip will be served the adjacent gzipped version instead (with Content-Encoding: gzip set), which will generally be smaller and served more quickly.

Response caching

 --max-age <maximum cache time in seconds>     (default 0, disabled)

You can enable caching by passing the --max-age <seconds> argument with a positive number of seconds to cache responses for. Responses to GET, HEAD and OPTIONS requests which include a Cache-Control: max-age=<seconds> header will be cached, for the specified number of seconds or the command-line argument, whichever is lower.

Responses that set cookies or are more than 1 megabyte in size are not cached.

The cache respects the Vary response header, which is used to indicate that a cache entry should only be used if the supplied list of request header fields are the same on cached and future requests.

For example, setting Vary: Cookie on a response header for a page in a customer account area will ensure that another customer, with a different session cookie, will not see a cached response belonging to the first customer.

The Vary: Cookie check will match the the entire Cookie request header, which may contain more than one cookie. You can supply an additional non-standard response header, X-WSGo-Vary-Cookies, to specify which individual cookies the page should vary on. This allows cookies such as tracking cookies, which don't affect the page content, to be ignored. For example:

X-WSGo-Vary-Cookies: sessionid, remember_me

A useful strategy is to add middleware to your application to add appropriate cache headers to every response if they are not already set. This might include:

  • setting Cache-control: max-age=60 on content that rarely changes.
  • setting Cache-control: no-cache on any request by a logged-in user.

Request prioritisation

Incoming requests are assigned a priority, and higher priority tasks will be served before lower ones, even if the lower one came in first.

Requests with a priority below a hardcoded threshold (currently -7000) will only run if no other workers are busy. Requests may time out without being handled, if the queue never emptied and their priority was insufficient for them to run within their request timeout.

A consequence of this is that there is a limit of five concurrent requests from the same IP (v4 /32 or v6 /64) per process.

The priority of a request is calculated as follows:

  • All requests start with a priority of 1000
  • Anything with a query string: -500
  • Each concurrent request from the same IPv4 or /64: -2000
  • Each historic request from the same IPv4 or /64: -1000 (decays by +1000/second)
  • User-Agent containing bot/crawler/spider/index: -8000

The priorities are recalculated everytime a request is grabbed from the queue.

Buffering

The first 1mb of POST/PUT/PATCH request bodies will be buffered before the WSGI handler is started.

Responses are not buffered.

It is therefore possible for users on slow connections to tie up handlers for a significant time during large uploads or downloads - if this is a concern then consider using a buffering load balancer upstream of wsgo.

Signals

You can send signals to running wsgo processes to cause them to report status information to stdout.

killall wsgo -s USR1 will print stack traces of all Python workers, which can be useful to diagnose hangs (such as every worker waiting for an unresponsive database with no timeout).

killall wsgo -s USR2 will print request and error count and memory statistics.

Cron-like system

The wsgo module provides two decorators:

@wsgo.cron(min, hour, day, mon, wday)

@wsgo.timer(period)

These should be used on top-level functions declared in the WSGI application Python file (or directly imported from it).

For example:

import wsgo

@wsgo.cron(30, -1, -1, -1, -1)
def runs_at_half_past_every_hour():
    print("Hi there!")

@wsgo.timer(30)
def runs_every_thirty_seconds():
    print("Hello again!")

If you are using more than one process, these will only be activated in the first one.

Blocking

You can block a source IP address from being handled for a limited time period by sending the response header:

X-WSGo-Block: <time in seconds>

Any subsequent requests from the same source IP, to the same process, within the time period, will be responded to with an empty 429 response. The responses will be delayed for 25s (or the remaining block time, whichever is lower), to attempt to slow down DoS attacks or crawlers, although a maximum of 100 simultaneous requests will be delayed so as to not exhaust open file handles.

Blocking is per-process, so if you are running multiple processes and want a block to apply across all of them, you will need a way to record blocks (eg, via a database) and reissue them if the visitor comes back via a different process, until the visitor is blocked in every process.

Blocked requests are not logged individually (to avoid DoS attacks overflowing the logs), but the total number of blocked requests will be reported when the block expires.

Request parking

A request parking mechanism allows for long-polling, where a request may be held deliberately for a long time before being responded to, without tying up a worker for each request.

A worker may respond immediately with the response headers:

X-WSGo-Park: channel1, channel2, channel3
X-WSGo-Park-Timeout: 60 http-204

This will cause the worker to finish, but the client will not be responded to. When the park timeout expires, the request will be handled according to the timeout action:

retry: The request will be retried.
disconnect: The connection will be dropped without a response.
http-204: The client will receive a HTTP 204 No Content response.
http-504: The client will receive a HTTP 504 Gateway Timeout response.

Parked requests can also be awakened via a function called from another thread:

wsgo.notify_parked(channels, action, arg)

where channels is a string containing a comma separated list of channels, action is one of wsgo.RETRY/wsgo.DISCONNECT/wsgo.HTTP_204/wsgo.HTTP_504, and arg is a string argument that, in the case of a retry, will be passed along with the request as the header X-WSGo-Park-Arg.

Any channel in the list of supplied channels that matches a channel of a waiting parked request will cause it to be awakened.

A retried request will be called with the same HTTP method and request body, with the additional X-WSGo-Park-Arg header (which in the case of a timeout retry may be a blank string). A retried request cannot be parked (to avoid the potential for eternally parked requests).

Each process handles its parked requests separately, so if you need to be able to awaken requests parked on one process from another process, you will need a separate asynchronous signalling mechanism between processes (such as PostgreSQL's LISTEN/NOTIFY) with a dedicated listening thread in each process.

Background

This project is heavily inspired by uWSGI, which the author has successfully used in production for many years. However it has some drawbacks:

  • It is difficult to configure correctly - the defaults are unsuitable and achieving reliablity and high performance requires a lot of trial-and-error.

  • It has a lot of protocol handling implemented in C, so has the potential for buffer overflow vulnerabilities (it has had one CVE due to this so far).

  • It lacks features like a page cache, which is understandable for larger deployments (where separate Varnish instances might be used) but would be nice to have built in for simpler deployments of smaller sites.

Notes

[^1]: An app that makes lots of database queries or API calls will likely benefit from multiple threads, as useful work can be done whilst some threads are blocked waiting for responses. However an app that is primarily CPU bound would be better with fewer threads, as more threads will incur unnecessary context switches and cache misses for little benefit.

A significant advantage of using multiple threads within a single process, as opposed to multiple processes, is that you can easily share data between workers (as they are all running in the same interpreter), which allows the use of fast local in-memory caches such as Django's LocMemCache.

Threads do carry some overhead - at least 8mb RAM per thread for the stack, as well as any thread-local resources such as open database connections. Also beware of hitting MySQL's 150 or PostgreSQL's 100 default maximum connections.

Multiple processes are completely isolated from each other, each with their own Python interpreter, request queue, block list and page cache. This is deliberate, to reduce the complexity of the system, and the chance of a deadlock spreading to all processes, but does reduce the effectiveness of using multiple processes rather than threads.

[^2]: Raising exceptions asynchronously in code that does not expect it can cause deadlocks, resulting in threads getting permanently 'stuck'. This includes Python's logging framework pre-3.13, which serializes all logging calls with a per-logger lock, and will leave the lock unreleased if an asynchronous exception occurs during the acquire. Whilst this shouldn't happen often, since the lock should only be held briefly during logging IO, it can occur reliably if the logging triggers slow database queries (such as Django error reports when outputting QuerySets).

wsgo mitigates this for the logging module, by attempting to release these locks after the request has finished, if an asynchronous exception was raised. Other libraries with this issue may require middleware to catch the `wsgo.RequestTimeoutException` and release the locks, or to always release the locks after every request just in case.

Roadmap

This project is currently being used in production, but still needs some tuning and has some missing features.

  • Is the threads-per-process count appropriate? It is deliberately quite high, but this may cause issues with the number of simultaneous database connections required. Also the GIL will prevent true multiprocessing, but then threading uses less memory than an equivalent number of processes.

  • Using Python 3.12's subinterpreters to allow concurrent Python execution inside the same process.

  • Deciding whether to add more features such as Websockets or ASGI.

  • The code still needs tidying up, and more tests writing.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

wsgo-0.0.25-cp314-cp314-manylinux_2_34_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.34+ x86-64

wsgo-0.0.25-cp314-cp314-manylinux_2_34_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.34+ ARM64

wsgo-0.0.25-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

wsgo-0.0.25-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ ARM64

wsgo-0.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

wsgo-0.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ ARM64

wsgo-0.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

wsgo-0.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ ARM64

wsgo-0.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ x86-64

wsgo-0.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ ARM64

wsgo-0.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.9manylinux: glibc 2.17+ x86-64

wsgo-0.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.9manylinux: glibc 2.17+ ARM64

wsgo-0.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.8manylinux: glibc 2.17+ x86-64

wsgo-0.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.8manylinux: glibc 2.17+ ARM64

wsgo-0.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.7mmanylinux: glibc 2.17+ x86-64

wsgo-0.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.7mmanylinux: glibc 2.17+ ARM64

wsgo-0.0.25-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.6mmanylinux: glibc 2.17+ x86-64

wsgo-0.0.25-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.6mmanylinux: glibc 2.17+ ARM64

File details

Details for the file wsgo-0.0.25-cp314-cp314-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp314-cp314-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 910350d54b8cc4a13d0eacb4e7f4902e121c87ebaf740cc3b28ed5fd98102fd8
MD5 ec7215949a28c5739a02cde33ca7bbad
BLAKE2b-256 d3538c10bc0870fa870224e6f291a29aa3cd15b989e18c21bbe5f6f8b1a28a0b

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp314-cp314-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp314-cp314-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 5750b8780124e8892808c4591f628d5d9f378abcf32385577fa9b4b2a2a0bd23
MD5 a1e76fecf06e6f8c53f0b20321e04d44
BLAKE2b-256 f2463d44b4e1ea4e1c5ace80cfc4c3e1be6d35a6eaa08b2643da576e7da180a1

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1b8f0dedd41992454387ad471b192ccdf58b77292f40ad872fbb886e81d8cba6
MD5 61b90354d5dcc9acdc2142e788601ce7
BLAKE2b-256 79558faf8c780395b8e12d0ca3dac0d529e7e64d0d9788ccf44fc12ed18f1482

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 86ef2cea2c1ce7592183454a2b4c8eef200580126ab006b287fba0d8215670af
MD5 72b69a9ce8ca94ab6981aaad8a6a9735
BLAKE2b-256 03636cc11ba04f71a0d8fce5ee4f5afc8a91430186a5d136ae5e328f09609a35

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 d0562c3270e3a3b2946b41cc679e91097bf90b9b42c2e6b3d66999806ad4b362
MD5 726e51a5f342db97852b851d606eeb97
BLAKE2b-256 76d1d2aa29ea03fd18dd01f9a57f09f1c30c91046099cabc4ebf26e8e44eb7cb

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 ec058df7d7d2bc9acde9416d3e18d22db84d9f9c9c825e16dbcd4bfb1a7f3408
MD5 e7cdc76a274c433c88fde429aada1d96
BLAKE2b-256 248e4f455ba2510084673e80f41cc36d4e408ce8f3c6303b74129557b8f793a6

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 38428caf041ca16a22b61e44aa67b54415a414dcdb4a478c06e2d274642ffd56
MD5 b911ac0dbb7bad0b1b38f89ef034f7cb
BLAKE2b-256 a31551c58ce5deb516fda9449c6edc5f2d3ccb9964446da4025d58eac4298cec

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 ec54f5085b6185181a5d8773f9094c523a3843540c480ac03d3dcf1d558512aa
MD5 a7e03fbdd57aaa27603b31807b889341
BLAKE2b-256 bf1458ed2526179f8390e24d27f02c61c3212ffde994a44751e91acac337f52a

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 7d1c238d73e2cfffabf891b15fd481c32e9a85cefb0b818a96cd4acb09b09771
MD5 dc11aa050d0e5969230a08d36fc19048
BLAKE2b-256 cdd3d398271eea942f7802e2d498fde28c628ec3812c152776783d376aaf7587

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 147de4588bdbb94ce9856a588862f29b7e57fa20bcd68d1984bad9a6a52decfb
MD5 2d090eb39a222a5a6b75e071dce208e7
BLAKE2b-256 1b32e3602077f9a04f58bfcc1f229d9b92ab064040031662984b6e9db9bb606f

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 2fbd3adb4b69d23b47573cfae994a2d1c8dee3b433921d0af954bd398a1069a8
MD5 50ac68b295d0d4b0c55309a34ff1acd3
BLAKE2b-256 3f6e489b7639c46b534f9fd8eb7048a57bf6592bf35d7115bfc3586415452d78

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 324fa3530f2d099d4cbf6c1ca6a634fc2a0f8a48a2cfc6990eaa4603d0e0a9b9
MD5 2b74e74bd0db33ec7ef340f5203a3d00
BLAKE2b-256 f41e7891ec98d0a46c4c2582edc647ea256c0fb84673ac77332316edee8a1b3c

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0dc94230a0793a3cbc26d7702b2f7566de2b5d5f9e038d03d65ba071ad893ef6
MD5 46e67f79780726c4f08e7d8170253a4a
BLAKE2b-256 4a3ff6cfa1c1abc461e1a8b1cb9af538dcea843b580532969c37afe0e0ef0c28

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 43c009b98a3789f54e93624d1d95e511ab6832daa5917269f2c54ac3b923f885
MD5 51b73ffed2fa805acd13e519a502f53f
BLAKE2b-256 88b1e847d99060e0b35750e632669709d2fdb4f0b2001d09f5f097b6d18f6b3c

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 ca68c30f3649524b2d982bba3f1121dc6d820346eb5c6a7307062404a3d78777
MD5 0ec3249ee3dc27b1a1319668fc0a99cb
BLAKE2b-256 bb9324a4fada1dea1ade29b75649c42b358be314d9041bed33dcfe26308ce450

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 ce15645a0d4d97a805399348f1e4390e07c1aa6db8b39f7eb6217be1ca74679a
MD5 29dd2a6e7387afd92d2fd19749c6d9b6
BLAKE2b-256 5acfd28f7c7a7e21f0c9358cf170e3a2ddf7d85a4e97fac1db87aa4ce08412fc

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e398030c28132fa15e103f7c6bb6d2ff9cf62be80abd2300daba80fe50393afc
MD5 c08d6c60ed0898a5e0ad2d37f53937b7
BLAKE2b-256 2aee9da9b7b407b708ce98d10011224d265c2663a931e409cd8fd14888171ecf

See more details on using hashes here.

File details

Details for the file wsgo-0.0.25-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.25-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 5c69545aaeba3a91beff6ece0e0baecba0e8dfc0c484db685704057e64649dd2
MD5 a04aeb8a9d26083232868e1a499dd1ca
BLAKE2b-256 3e17428dd736a6d2e47acaac16362d3e1b0765f2344d9c4fc3d7849ed2d86cd4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page