encode / starlette Goto Github PK
View Code? Open in Web Editor NEWThe little ASGI framework that shines. ๐
Home Page: https://www.starlette.io/
License: BSD 3-Clause "New" or "Revised" License
The little ASGI framework that shines. ๐
Home Page: https://www.starlette.io/
License: BSD 3-Clause "New" or "Revised" License
The App and Router classes should support a .url_for(name, **kwargs)
method.
We'll probably need to finesse our routing a bit here, eg. remove the ability to drop into plain regex, and instead add something like {username:str}
/ {page_number:int}
.
Eventually we'll probably also want to support namespacing eg. app.url_for('users:create_user', username=foo)
Important
Is there any cons for inherit Response
from Exception
to be able to raise it?
Refs django/asgiref#63
Add @app.startup
and @app.shutdown
decorators that call into code on lifespan startup/shutdown.
Eg...
app = Starlette()
@app.startup
async def initialize_database_connection_pool():
...
@app.shutdown
async def close_database_connection():
...
You'll be able to add multiple startup/shutdown events.
Should we consider dropping the implicit use of ujson
, and instead include a UJSONResponse
class so that it needs to be used explicitly instead? I'd tend to think that by default you'll want to prefer using the stdlib json
since it's a bit tighter on it's behaviour when eg. handling unexpected types.
It might well make sense to only use ujson
explicitly (so eg. users can switch to it if they have particular known endpoints that're already well-tested but have a non-insignificant serialization overhead).
(In micro-benchmarking ujson is super important, but in real use-cases it generally isn't.)
ExceptionMiddleware
class.HTTPException(status_code=..., detail=...)
class.App
.This relates to discussions in #17 - figured an issue better than that PR for working this out.
'HEAD'
in the supported methods if 'GET'
is there.'HEAD'
requests without sending the entire file.Important
Implement a session middleware that adds a mutable dict-like โsessionโ interface into the scope.
The interface should track if it has been mutated. Loading the session data given the incoming request cookies, and saving any modified sessions in outgoing response cookies should be handled by a SessionBackend, so that we separate out the session load/save mechanics from the session interface itself.
Hi looks like the middleware directory is absent from the package on PyPI.
I'm developing an app that requires async setup and teardown during testing. For example, I insert data into a Redis database using aioredis. I'm using pytest-asyncio to mark async test functions.
The test client uses the synchronous requests API and an ASGI adapter to call the Starlette app. The setup I described above doesn't work with the adapter because it doesn't expect an event loop to be running.
I haven't worked with asyncio prior to using Starlette so I may be missing something obvious but do you have any suggestions for how to deal with this? I thought about more explicitly managing the event loop during testing but this seemed a bit tedious.
Another option was an async test client. I had a look at aiohttp and if a custom Connector
could be used in place of the ASGI requests adapter but I didn't spend much time on it.
For now, I cobbled together an async test client that combines the request setup from requests.Session
and starlette.testclient. _ASGIAdapter
.
By the way, if there's a better place to ask questions, please let me know ๐
In both our function and class-based endpoints, we're currently using function signatures like:
def handler(request, **kwargs):
...
Which are sometimes expanded to include the actual path keyword arguments that are being routed, eg...
@app.route('/{username}')
def userpage(request, username):
...
I think we should possibly move against the grain of what Python frameworks are all doing here, and instead have the routing be part of the request itself, so... request.kwargs.username
That'll give us really simple, consistent interfaces, that don't sometimes look like handler(request, **kwargs)
and other times look like handler(request, username=None)
etc...
It's also consistent with say, things like request.auth
and request.user
that we may eventually have - they're other bits of information on the request that are just as important as the routing keyword arguments, but they just live on the request object itself.
It'll also simplify things like our class-based interfaces where some of the methods take **kwargs
and others don't.
Not typical of other frameworks, but I quite like it as a more refined, consistent interface.
There are plenty of places where we should add type annotations...
mypy starlette --disallow-untyped-defs
starlette/datastructures.py:7: error: Function is missing a type annotation
starlette/datastructures.py:13: error: Function is missing a type annotation
starlette/datastructures.py:17: error: Function is missing a type annotation
starlette/datastructures.py:21: error: Function is missing a type annotation
starlette/datastructures.py:25: error: Function is missing a type annotation
...
For the purposes of this ticket, ignore the error cases (see #6) since they're pretty tricky, and just focus on including any missing annotations.
It'd be helpful if CORSMiddleware
supported an allow_origin_regex
, so that users could do...
# Enforce a subdomain CORS policy
app.add_middleware(CORSMiddleware, allow_origin_regex="(http|https)://*.example.com")
Or...
# Enforce an HTTPS-only CORS policy.
app.add_middleware(CORSMiddleware, allow_origin_regex="https://*")
The string should be compiled to a regex by the middleware and matches should be anchored to the start/end of the origin string.
See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Credentialed_requests_and_wildcards
If a standard request is made, that includes any cookie headers, then CORSMiddleware ought to strictly respond with the requested origin, rather than a wildcard.
This is actually potentially a bit fiddly since we maybe also need to make sure to set or add Vary: Origin in those cases, in order to ensure correct cacheability.
The request and response docs should document cookie usage. ๐ช
Add request.cookies
and response.cookies
.
Not much work to do, since https://docs.python.org/3/library/http.cookies.html supports everything we'd need.
We shouldn't create a request.headers
instance unless it is accessed.
Right now, Router
is simply an ASGI app, which means that it can be used as a top-level app; however, it can't benefit from Starlette
-specific features such as debug
, lifespan events or exception handlers.
Would it make sense to make Router
a subclass of Starlette
? That way instead of:
app = Starlette()
app.mount('/', Router((
Path('product/{product_id}', app=ProductEndpoint),
Path('product', app=NewProductEndpoint),
Path('products', app=ProductListEndpoint),
)))
we can have
app = Router((
Path('product/{product_id}', app=ProductEndpoint),
Path('product', app=NewProductEndpoint),
Path('products', app=ProductListEndpoint),
))
and keep all the advantages of Starlette
.
This is just a simplified example, of course.
Introduce multipart and URL encoded parsing to support form = await request.form()
I am very interested in contributing to some common middleware for ASGI, and this seems like it is the perfect place to do that, however I have a few questions about how much Starlette is aiming to achieve:
How much functionality will this project implement?
How much functionality does it intend to leave up to particular frameworks?
Will it potentially become a more traditional framework itself, or is the intention to remain a small library?
Refs #102
Static files larger than 4096 bytes do not appear to be served correctly.
Here's a test I just wrote that illustrates the problem: simonw@e2d6665
Running that test gives me the following output:
(venv) starlette $ PYTHONPATH=. pytest -k test_large_staticfile
===================================================== test session starts ======================================================
platform darwin -- Python 3.6.5, pytest-3.6.1, py-1.5.3, pluggy-0.6.0
rootdir: /Users/simonw/Dropbox/Development/starlette, inifile:
collected 43 items / 42 deselected
tests/test_staticfiles.py F [100%]
=========================================================== FAILURES ===========================================================
____________________________________________________ test_large_staticfile _____________________________________________________
tmpdir = local('/private/var/folders/jj/fngnv0810tn2lt_kd3911pdc0000gp/T/pytest-of-simonw/pytest-8/test_large_staticfile0')
def test_large_staticfile(tmpdir):
path = os.path.join(tmpdir, "example.txt")
content = "this is a lot of content" * 200
print("content len = ", len(content))
with open(path, "w") as file:
file.write(content)
app = StaticFile(path=path)
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
> assert len(content) == len(response.text)
E AssertionError: assert 4800 == 4096
E + where 4800 = len('this is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of cont...is is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of content')
E + and 4096 = len(' contentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot...ontentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of')
E + where ' contentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot...ontentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of contentthis is a lot of' = <Response [200]>.text
tests/test_staticfiles.py:30: AssertionError
We should probably hide away Path
and Include
as implementation details for now, and instead focus on making the app and router interfaces match up.
Eg:
app = Router()
@app.route('/')
async def homepage(...):
...
or...
app = Router()
app.mount('/admin', AdminApp)
app.mount('/articles', BlogArticles)
This'll mean that a Router
instance strictly gives you a subset of the functionality that a Starlette
instance offers.
Refs #122
Are there any plans to include a way to attach objects which share an app's lifetime, for example database connections and similar? Or is that supposed to be handled my middleware somehow? Thanks!
HEAD
if GET
is in methods
.Allow
header on responses.App
callable while still leaving the app instance visible.We have FileResponse
and StaticFiles
.
I think that including the StaticFile
ASGI app complicates things unnecessarily, and that we should probably remove it.
StaticFile
app.FileResponse
.There are a few remaining mypy errors that it'd be nice to tidy up...
$ mypy starlette
starlette/datastructures.py:148: error: Signature of "get" incompatible with supertype "Mapping"
starlette/datastructures.py:158: error: Argument 1 of "__contains__" incompatible with supertype "Mapping"
starlette/datastructures.py:158: error: Argument 1 of "__contains__" incompatible with supertype "Container"
starlette/testclient.py:114: error: Signature of "request" incompatible with supertype "Session"
The datastructures ones really aren't obvious. It may be that they're resolvable but I've not figured out how.
The testclient one we might want to just put an exclude against - otherwise we need to include all of the keyword arguments to request explicitly, rather than capturing them with **kwargs
and passing them on.
HEAD
304
if ETag and Last-Modified indicate no changes against a conditional GET
request.App
first in README / Docs intro.app = App()
style, but then go on to demonstrate plain ASGI style example, and discuss design philosophy of "everything is just ASGI" , interoperable, eg. use TestClient
with Channels.WebSocketSession
as just WebSocket
? Rename session
variable as websocket
in docs?StaticFile
, and just leave StaticFiles
and FileResponse
.starlette.requests
/starlette.responses
instead of starlette.request
/starlette.response
? Would fit websockets
and align better with documentation titles.App
should perhaps be Starlette
instead. Less likely to be confused with plain ASGI App
class examples, and mirrors eg. flask.Flask
and sanic.Sanic
.HTTPEndpoint
and WebSocketEndpoint
?Ensure that requests
is only required if TestClient
is used. Ensure that aiofiles
is only required if FileResponse
/StaticFiles
is used. We could choose to push FileResponse
into the starlette.staticfiles
module, and only ever import aiofiles
from there.
See https://github.com/encode/starlette/milestone/1
Important
Since you are advertising the use of ujson, I was expecting that it is being used as a drop-in everywhere in starlette. When I post to a simple route handler without any body content, I see an exception coming from the standard library, however. Is this a special case when there is no body content or is ujson only used in serializing the JSONResponse
?
Code:
@app.route('/', methods=["POST"], )
async def search(request: Request) -> JSONResponse:
query = await request.json()
return query
Traceback:
web_1 | query = await request.json()
web_1 | File "/usr/local/lib/python3.7/site-packages/starlette/requests.py", line 96, in json
web_1 | self._json = json.loads(body)
web_1 | File "/usr/local/lib/python3.7/json/__init__.py", line 348, in loads
web_1 | return _default_decoder.decode(s)
web_1 | File "/usr/local/lib/python3.7/json/decoder.py", line 337, in decode
web_1 | obj, end = self.raw_decode(s, idx=_w(s, 0).end())
web_1 | File "/usr/local/lib/python3.7/json/decoder.py", line 355, in raw_decode
web_1 | raise JSONDecodeError("Expecting value", s, err.value) from None
web_1 | json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
It seems that even with all tests passing and cors being successfully applied, CORSMiddleware still raises a runtime error.
Code being tested:
app = Starlette()
app.add_middleware(CORSMiddleware, allow_origins=["*"])
@app.route("/")
async def homepage(request):
return PlainTextResponse('Hello', status_code=200)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
And the error being produced:
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/home/alexbotello/.local/share/virtualenvs/starlette-dshJy1CJ/lib/python3.7/site-packages/uvicorn/protocols/http/httptools_impl.py", line 384, in run_asgi
result = await asgi(self.receive, self.send)
File "/home/alexbotello/Code/starlette/starlette/exceptions.py", line 60, in app
raise exc from None
File "/home/alexbotello/Code/starlette/starlette/exceptions.py", line 52, in app
await instance(receive, sender)
File "/home/alexbotello/Code/starlette/starlette/middleware/cors.py", line 116, in simple_response
await inner(receive, send)
File "/home/alexbotello/Code/starlette/starlette/applications.py", line 26, in awaitable
await response(receive, send)
File "/home/alexbotello/Code/starlette/starlette/responses.py", line 100, in __call__
await send({"type": "http.response.body", "body": self.body})
File "/home/alexbotello/Code/starlette/starlette/middleware/cors.py", line 130, in send
await send(message)
File "/home/alexbotello/Code/starlette/starlette/exceptions.py", line 47, in sender
await send(message)
File "/home/alexbotello/.local/share/virtualenvs/starlette-dshJy1CJ/lib/python3.7/site-packages/uvicorn/protocols/http/httptools_impl.py", line 518, in send
raise RuntimeError(msg % message_type)
RuntimeError: Unexpected ASGI message 'http.response.body' sent, after response already completed.
It seems the issue is originating from send
. Specifically:
if message["type"] != "http.response.start":
await send(message)
Removing this fixes the issue and does not break any tests.
without manually setting the Connection HTTP header in a response, Apache Bench (Version 2.3 <$Revision: 1528965 $>) reports 5000ms + connection times.
If you manually set the Connection HTTP header to either "Keep-Alive" or "close" then ab
shows results as expected (1ms connection time).
e.g.
$ ab -v 2 -c 1 -n 1 http://localhost:5044/
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)...INFO: GET header ==
---
GET / HTTP/1.0
Host: localhost:5044
User-Agent: ApacheBench/2.3
Accept: */*
---
LOG: header received:
HTTP/1.1 200 OK
server: uvicorn
date: Thu, 18 Oct 2018 21:33:50 GMT
transfer-encoding: chunked
6
string
0
..done
Server Software:
Server Hostname: localhost
Server Port: 5044
Document Path: /
Document Length: 16 bytes
Concurrency Level: 1
Time taken for tests: 5.001 seconds
Complete requests: 1
Failed requests: 0
Total transferred: 139 bytes
HTML transferred: 16 bytes
Requests per second: 0.20 [#/sec] (mean)
Time per request: 5001.050 [ms] (mean)
Time per request: 5001.050 [ms] (mean, across all concurrent requests)
Transfer rate: 0.03 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 5001 5001 0.0 5001 5001
Waiting: 1 1 0.0 1 1
Total: 5001 5001 0.0 5001 5001
$
$ ab -v 2 -c 1 -n 1 http://localhost:5044/
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)...INFO: GET header ==
---
GET / HTTP/1.0
Host: localhost:5044
User-Agent: ApacheBench/2.3
Accept: */*
---
LOG: header received:
HTTP/1.1 200 OK
server: uvicorn
date: Thu, 18 Oct 2018 21:39:02 GMT
connection: close
transfer-encoding: chunked
6
string
0
..done
Server Software:
Server Hostname: localhost
Server Port: 5044
Document Path: /
Document Length: 16 bytes
Concurrency Level: 1
Time taken for tests: 0.001 seconds
Complete requests: 1
Failed requests: 0
Total transferred: 136 bytes
HTML transferred: 16 bytes
Requests per second: 677.05 [#/sec] (mean)
Time per request: 1.477 [ms] (mean)
Time per request: 1.477 [ms] (mean, across all concurrent requests)
Transfer rate: 89.92 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 1 1 0.0 1 1
Waiting: 1 1 0.0 1 1
Total: 1 1 0.0 1 1
code:
from starlette.applications import Starlette
from starlette.responses import JSONResponse, Response
import uvicorn
app = Starlette()
@app.route('/')
async def index(request):
return Response('string', headers={'Connection': 'close'})
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=5044)
Right now we just display a raw format_trackback()
.
It'd make sense to have a proper page with lines of context around each file in the stack, and local variables at each point. (As with Django or Werkzeug's debugger pages)
requests
is only required at the point of instantiating TestClient
.aiofiles
is only required at the point of instantiating FileResponse
or StaticFiles
.background=...
keyword argument to all responses. It should be either None
or a coroutine with no arguments.__call__
should run the background coroutine if it exists (after sending the response).BackgroundTask()
class that wraps a callable and some arguments. This can then be used to pass a function or coroutine with args/kwargs to a response.class BackgroundTask:
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
async def __call__(self):
if asyncio.iscoroutine(self.func):
await self.func(*self.args, **self.kwargs)
else:
self.func(*self.args, **self.kwargs)
Then we can do...
return Response(..., background=BackgroundTask(my_func, a=123)
Naming might need some thinking about - not sure that conflicting with asyncio's Task
is ideal.
This'd be a good feature to get in, since it's a low amount of work, but provides functionality that WSGI is unable to do.
Helpers for sending SSE event streams over HTTP connections.
Related resources:
Requests should possibly expose a MutableMapping
interface, rather than Mapping
.
Although we present immutable interfaces on the request properties, it'd make sense for requests to more correctly match the scope
interface, and allow dict mutation.
We'd need to take some care to invalidate cached properties if we did so, particularly if request['headers']
is mutated.
I'm not sure this is the best place to ask these, so feel free to point me somewhere else.
asyncio.get_event_loop()
? (I'm developing on Python 3.6.)from concurrent.futures import ThreadPoolExecutor
? Would it be possible to maintain such a pool on the app rather than spawning one each time a request is made (which seems like a terrible idea).The Request
class should present a dict-like interface so that it can be used in the same way as scope
. Should also allow it to be instantiated without a receive
channel being set initially.
Provide a class-based websocket handler.
from starlette.endpoints import WebSocketEndpoint
class App(WebSocketEndpoint):
async def on_connect(self, websocket, **kwargs):
...
async def on_receive(self, websocket, data):
...
async def on_disconnect(self, websocket):
...
From https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope
server: A two-item iterable of [host, port], where host is the listening address for this server as a unicode string, and port is the integer listening port. Optional, defaults to None.
https://github.com/encode/starlette/blob/master/starlette/datastructures.py#L11 doesn't handle that option, it assumes scope["server"] is always a two-pair
Related to #1
I'm having trouble figuring out an appropriate pattern for including CORS support. My use-case is that I am connecting a React app to an ASGI backend that uses Starlette's routing and response classes, but enabling CORS support would require altering the response class headers which cannot be accomplished simply by wrapping the ASGI app at this point.
What should CORS support look like in Starlette?
An example of my use-case can be found here, it uses APIStar and a CORS extension to achieve what I want to do with Starlette as well. Below is my work-in-progress attempt at an ASGI CORS middleware that I would ultimately like to work into Starlette somehow.
from starlette import PlainTextResponse
from starlette.datastructures import Headers
ACCESS_CONTROL_ALLOW_ORIGIN = b"Access-Control-Allow-Origin"
ACCESS_CONTROL_EXPOSE_HEADERS = b"Access-Control-Expose-Headers"
ACCESS_CONTROL_ALLOW_CREDENTIALS = b"Access-Control-Allow-Credentials"
ACCESS_CONTROL_ALLOW_HEADERS = b"Access-Control-Allow-Headers"
ACCESS_CONTROL_ALLOW_METHODS = b"Access-Control-Allow-Methods"
ACCESS_CONTROL_MAX_AGE = b"Access-Control-Max-Age"
ACCESS_CONTROL_REQUEST_METHOD = b"Access-Control-Request-Method"
ACCESS_CONTROL_REQUEST_HEADERS = b"Access-Control-Request-Headers"
DEFAULT_OPTIONS = {
"allow_origin": [b"*"],
"allow_credentials": False,
"allow_headers": [b"*"],
"allow_methods": [
b"GET",
b"HEAD",
b"POST",
b"OPTIONS",
b"PUT",
b"PATCH",
b"DELETE",
],
"expose_headers": [b""],
"max_age": b"86400",
}
class CORSMiddleware:
def __init__(self, app, options=None):
self.app = app
self.options = options or DEFAULT_OPTIONS
def __call__(self, scope):
headers = Headers(scope.get("headers", []))
origin = headers.get("origin", None)
if origin is None:
return self.app(scope)
allow_origin = self.options["allow_origin"]
if origin not in allow_origin and b"*" not in allow_origin:
return PlainTextResponse("Origin not allowed", status_code=400)
if scope["method"] == "OPTIONS":
method = headers.get(ACCESS_CONTROL_REQUEST_METHOD, None)
if method is None:
return PlainTextResponse(
"Access-Control-Request-Method header missing", status_code=400
)
cors_headers = []
if origin != "*" and len(allow_origin) > 1: # todo double-check this
cors_headers.append((b"vary", b"origin"))
cors_allow_origin = b", ".join(self.options["allow_origin"])
cors_allow_credentials = self.options["allow_credentials"]
cors_allow_headers = b", ".join(self.options["allow_headers"])
cors_allow_methods = b", ".join(self.options["allow_methods"])
cors_expose_headers = b", ".join(self.options["expose_headers"])
cors_max_age = self.options["max_age"]
cors_headers.append((ACCESS_CONTROL_ALLOW_ORIGIN, cors_allow_origin))
cors_headers.append((ACCESS_CONTROL_ALLOW_METHODS, cors_allow_methods))
cors_headers.append((ACCESS_CONTROL_ALLOW_HEADERS, cors_allow_headers))
cors_headers.append((ACCESS_CONTROL_ALLOW_CREDENTIALS, cors_allow_credentials))
cors_headers.append((ACCESS_CONTROL_EXPOSE_HEADERS, cors_expose_headers))
cors_headers.append((ACCESS_CONTROL_MAX_AGE, cors_max_age))
scope["headers"].extend(cors_headers)
print(scope["headers"])
return self.app(scope)
Currently no tooling provided here for working with websockets.
Need to have a think about what we can usefully add there.
cleanup
or shutdown
as the ASGI lifespan message name.cleanup
Easy PR for a contributor to jump on would be addressing the first part of this, and supporting either name.
Like most CORS implementations, we leave enforcement strictly as a browser concern, and just make sure that we're setting the correct headers for the browser to deal with that.
If we wanted we could strictly enforce the policy server-side too. That might be useful for eg. ensuring that it's easier to test, that developers get logs that reflect any cors failures, etc...
We do some similar checks in preflight_response
, which does provide useful informative responses, either 400 or 200.
If we add server-side enforcement, then I think it should probably be optional, but default to being on. enforce_server_side=True
.
We'd want to use the same sort of checking, and useful errors as we currently do in preflight_response
, and accumulate the different error types (origin, method, headers, credentials).
Also worth noting that preflight_response
doesn't currently check for the existence of cookies
against allow_credentials
. (It doesn't strictly need to but if we're going to add server-side enforcement then it ought to be used in both places.)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.