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

wsgo-0.0.22-cp313-cp313-manylinux_2_34_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.13 manylinux: glibc 2.34+ ARM64

wsgo-0.0.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.13 manylinux: glibc 2.17+ x86-64

wsgo-0.0.22-cp312-cp312-manylinux_2_34_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.34+ ARM64

wsgo-0.0.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64

wsgo-0.0.22-cp311-cp311-manylinux_2_34_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.34+ ARM64

wsgo-0.0.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64

wsgo-0.0.22-cp310-cp310-manylinux_2_34_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.34+ ARM64

wsgo-0.0.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ x86-64

wsgo-0.0.22-cp39-cp39-manylinux_2_34_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.9 manylinux: glibc 2.34+ ARM64

wsgo-0.0.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.9 manylinux: glibc 2.17+ x86-64

wsgo-0.0.22-cp38-cp38-manylinux_2_34_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.8 manylinux: glibc 2.34+ ARM64

wsgo-0.0.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.8 manylinux: glibc 2.17+ x86-64

wsgo-0.0.22-cp37-cp37m-manylinux_2_34_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.7m manylinux: glibc 2.34+ ARM64

wsgo-0.0.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.7m manylinux: glibc 2.17+ x86-64

wsgo-0.0.22-cp36-cp36m-manylinux_2_34_aarch64.whl (4.0 MB view details)

Uploaded CPython 3.6m manylinux: glibc 2.34+ ARM64

wsgo-0.0.22-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB view details)

Uploaded CPython 3.6m manylinux: glibc 2.17+ x86-64

File details

Details for the file wsgo-0.0.22-cp313-cp313-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.22-cp313-cp313-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 981f2bc0857ea3691cc4460bfb40fae81d4408ba9840c46b8048835209a8154a
MD5 515c14e7a935817a6311e2817a038455
BLAKE2b-256 1114470f35f171994458ca7abea42dc09f60202d19e9ba66656ccf97cddea6de

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wsgo-0.0.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1f9f447af0c2655c7908b534902cdf3cd2d17718467fbc485b93c107587dcbc3
MD5 03dc7f6e5ffc2c8df03928a73daebe20
BLAKE2b-256 20081f7cf051e5ab7d82aa591853ce6c1b2c18c8f518598d6faf673fe6b5b8ed

See more details on using hashes here.

File details

Details for the file wsgo-0.0.22-cp312-cp312-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.22-cp312-cp312-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 99a882f10e3040841eab5d07de47ec0cb54e1119fc77511fda319a65b47cb256
MD5 3dfa6b66f995677dd61b7ed2b978f8cf
BLAKE2b-256 06a7612e0547a2c840367b2eeb1a2e343caff2525b475b00243fcd29def1af1e

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wsgo-0.0.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 47ec61cbb409730b384fd516699081866f44ae9901a712b6fa634559fa9f1a93
MD5 46de4a93d10b7cd5d2149c5f7c85a293
BLAKE2b-256 79d77a808f42b6f10d0fafbd6fb8b18b76766bc821175b64b4e6074b438530c8

See more details on using hashes here.

File details

Details for the file wsgo-0.0.22-cp311-cp311-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.22-cp311-cp311-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 4b8be805dcb32d5a21665c28c5829f8a7112ebaf54e9a6772f31a4a942ecf33d
MD5 b298862218d5cae18d19805b5849197d
BLAKE2b-256 a3b1b47bdd9b713955df94e9f6839e0cb6f659729310be3a49eb6c07785c04e7

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wsgo-0.0.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 bd6258b2600024786163fefc960ee4c5fb73bee06fc29e442461604e8b5e48d7
MD5 b893c562b86f802d52fb2a19f2c99ba5
BLAKE2b-256 0da7772c16e2b95e90caeef5147c2264a63d108062b1108bfd00fd7ac50fede1

See more details on using hashes here.

File details

Details for the file wsgo-0.0.22-cp310-cp310-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.22-cp310-cp310-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 a3540c13ba61e5db25690467bd720ed161946d86b5c79995bb992d90b0fa7718
MD5 7f647c455a4226f0e2411ef55f43e05d
BLAKE2b-256 b6509165bb1e39132f24be7a987adbf379e315d9e1b1270bf1c6537f79ec02fc

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wsgo-0.0.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 56b8c658d2db5d6a1c7376dc1218f143011a365f41290a0b561fbaf8eca7e50c
MD5 5b45b307bc057e549e77d7f401a9c63f
BLAKE2b-256 fc98dc42f5ee7932d4cd8a2385554372791ce9a83182a909fde68bb277db8089

See more details on using hashes here.

File details

Details for the file wsgo-0.0.22-cp39-cp39-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.22-cp39-cp39-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 f97afae2bda3a5a0d6c8230e916d1fcf6f3d8cc2201316e03273b7d45ec94c0c
MD5 f36a8f3580df455d2ccbf9268fe31767
BLAKE2b-256 2e46e700fe01f28f57c5f944f1f16400b2d41bf1f69770b717a108280922b841

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wsgo-0.0.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 8ed917d58be0ea2a4ee6f66c2dc6cf274312e83e37f13d1a4c1d1919a1b0a4e6
MD5 ef7bf261a57d86a6b1d4ad634ede17cd
BLAKE2b-256 17da1c05ae488c95450a7cc95f1862a3695afa16f67f1b2d677d9c7c97a545f6

See more details on using hashes here.

File details

Details for the file wsgo-0.0.22-cp38-cp38-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.22-cp38-cp38-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 e5f36d135b43b03ed83e3a4dd2bf6930e3486d43ac442dc32997caeb3774cfaf
MD5 6fac6fa6e6d39d4546f3151b4d485728
BLAKE2b-256 05664cd4b723db52e79991e876a16a7369bcd61bc57b6ad4c14a6408bcb95ff6

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wsgo-0.0.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 857768e17ec1c1c737f0231fca3dd68ddcfa53004707c5f3b3c60237924394c2
MD5 48cddc054ac4bd9ec2e32d838695b4e6
BLAKE2b-256 b39a627a9c9cc9ce5509940e55f9b315367b65f446b95c5f5d6ae027dc5978c1

See more details on using hashes here.

File details

Details for the file wsgo-0.0.22-cp37-cp37m-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.22-cp37-cp37m-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 eff39b7fae140a992c7c431d3e88672c668ddddb3fdfac46a0a5c72cf0ff5f32
MD5 0b6c8bfbbf9cfdbeec9b480583e2834a
BLAKE2b-256 6e7d4cca059861416a9656afddcd7470d0bc6de895658235f60f2b1773c4c884

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wsgo-0.0.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 397cecc56cc7896dcd24383a241c596d86aaeeb0b45715b0e58f0057bd1c1560
MD5 0c2b01ffb3d00e2914c69d3ff4e8672d
BLAKE2b-256 30e8a0391f8d36f4d979ac1707a27cd59eae93ce2944599f3170f9cb1a7c6e1e

See more details on using hashes here.

File details

Details for the file wsgo-0.0.22-cp36-cp36m-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for wsgo-0.0.22-cp36-cp36m-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 7e7f12c0969d8ea0ed78b94ebd8f6b577bfb58251caab33387200edf66bd1819
MD5 a59792c2e48336380520679713fbe41e
BLAKE2b-256 e66eb0fdb05379174939807267229c89f23634e3db254f161ebbad08da56bd1f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wsgo-0.0.22-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1177894c93a54d585fdb539a36f83097a62a1aaa2318f2a03665e69f26855ca1
MD5 892584810ed1e0e4a3ccaf7eda6181df
BLAKE2b-256 22eeb8b7ca8332db0dc5586e7128e7cddaf5f3148a4ff620b8c6a05854e4635c

See more details on using hashes here.

Supported by

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