Giter VIP home page Giter VIP logo

sanic-plugin-toolkit's Introduction

Sanic Plugin Toolkit

Build Status Latest Version Supported Python versions License

Welcome to the Sanic Plugin Toolkit.

The Sanic Plugin Toolkit (SPTK) is a lightweight python library aimed at making it as simple as possible to build plugins for the Sanic Async HTTP Server.

The SPTK provides a SanicPlugin python base object that your plugin can build upon. It is set up with all of the basic functionality that the majority of Sanic Plugins will need.

A SPTK Sanic Plugin is implemented in a similar way to Sanic Blueprints. You can use convenience decorators to set up all of the routes, middleware, exception handlers, and listeners your plugin uses in the same way you would a blueprint, and any Application developer can import your plugin and register it into their application.

The Sanic Plugin Toolkit is more than just a Blueprints-like system for Plugins. It provides an enhanced middleware system, and manages Context objects.

Notice: Please update to SPTK v0.90.1 if you need compatibility with Sanic v21.03+.

The Enhanced Middleware System

The Middleware system in the Sanic Plugin Toolkit both builds upon and extends the native Sanic middleware system. Rather than simply having two middleware queues ('request', and 'response'), the middleware system in SPF uses five additional queues.

  • Request-Pre: These middleware run before the application's own request middleware.
  • Request-Post: These middleware run after the application's own request middleware.
  • Response-Pre: These middleware run before the application's own response middleware.
  • Response-Post: These middleware run after the application's own response middleware.
  • Cleanup: These middleware run after all of the above middleware, and are run after a response is sent, and are run even if response is None.

So as a plugin developer you can choose whether you need your middleware to be executed before or after the application's own middleware.

You can also assign a priority to each of your plugin's middleware so you can more precisely control the order in which your middleware is executed, especially when the application is using multiple plugins.

The Context Object Manager

One feature that many find missing from Sanic is a context object. SPF provides multiple context objects that can be used for different purposes.

  • A shared context: All plugins registered in the SPF have access to a shared, persistent context object, which anyone can read and write to.
  • A per-request context: All plugins get access to a shared temporary context object anyone can read and write to that is created at the start of a request, and deleted when a request is completed.
  • A per-plugin context: All plugins get their own private persistent context object that only that plugin can read and write to.
  • A per-plugin per-request context: All plugins get a temporary private context object that is created at the start of a request, and deleted when a request is completed.

Installation

Install the extension with using pip, or easy_install.

$ pip install -U sanic-plugin-toolkit

Usage

A simple plugin written using the Sanic Plugin Toolkit will look like this:

# Source: my_plugin.py
from sanic_plugin_toolkit import SanicPlugin
from sanic.response import text

class MyPlugin(SanicPlugin):
    def __init__(self, *args, **kwargs):
        super(MyPlugin, self).__init__(*args, **kwargs)
        # do pre-registration plugin init here.
        # Note, context objects are not accessible here.

    def on_registered(self, context, reg, *args, **kwargs):
        # do post-registration plugin init here
        # We have access to our context and the shared context now.
        context.my_private_var = "Private variable"
        shared = context.shared
        shared.my_shared_var = "Shared variable"

my_plugin = MyPlugin()

# You don't need to add any parameters to @middleware, for default behaviour
# This is completely compatible with native Sanic middleware behaviour
@my_plugin.middleware
def my_middleware(request)
    h = request.headers
    #Do request middleware things here

#You can tune the middleware priority, and add a context param like this
#Priority must be between 0 and 9 (inclusive). 0 is highest priority, 9 the lowest.
#If you don't specify an 'attach_to' parameter, it is a 'request' middleware
@my_plugin.middleware(priority=6, with_context=True)
def my_middleware2(request, context):
    context['test1'] = "test"
    print("Hello world")

#Add attach_to='response' to make this a response middleware
@my_plugin.middleware(attach_to='response', with_context=True)
def my_middleware3(request, response, context):
    # Do response middleware here
    return response

#Add relative='pre' to make this a response middleware run _before_ the
#application's own response middleware
@my_plugin.middleware(attach_to='response', relative='pre', with_context=True)
def my_middleware4(request, response, context):
    # Do response middleware here
    return response

#Add attach_to='cleanup' to make this run even when the Response is None.
#This queue is fired _after_ response is already sent to the client.
@my_plugin.middleware(attach_to='cleanup', with_context=True)
def my_middleware5(request, context):
    # Do per-request cleanup here.
    return None

#Add your plugin routes here. You can even choose to have your context passed in to the route.
@my_plugin.route('/test_plugin', with_context=True)
def t1(request, context):
    words = context['test1']
    return text('from plugin! {}'.format(words))

The Application developer can use your plugin in their code like this:

# Source: app.py
from sanic import Sanic
from sanic_plugin_toolkit import SanicPluginRealm
from sanic.response import text
import my_plugin

app = Sanic(__name__)
realm = SanicPluginRealm(app)
assoc = realm.register_plugin(my_plugin)

# ... rest of user app here

There is support for using a config file to define the list of plugins to load when SPF is added to an App.

# Source: sptk.ini
[plugins]
MyPlugin
AnotherPlugin=ExampleArg,False,KWArg1=True,KWArg2=33.3
# Source: app.py
app = Sanic(__name__)
app.config['SPTK_LOAD_INI'] = True
app.config['SPTK_INI_FILE'] = 'sptk.ini'
realm = SanicPluginRealm(app)

# We can get the assoc object from SPF, it is already registered
assoc = spf.get_plugin_assoc('MyPlugin')

Or if the developer prefers to do it the old way, (like the Flask way), they can still do it like this:

# Source: app.py
from sanic import Sanic
from sanic.response import text
from my_plugin import MyPlugin

app = Sanic(__name__)
# this magically returns your previously initialized instance
# from your plugin module, if it is named `my_plugin` or `instance`.
assoc = MyPlugin(app)

# ... rest of user app here

Contributing

Questions, comments or improvements? Please create an issue on Github

Credits

Ashley Sommer [email protected]

sanic-plugin-toolkit's People

Contributors

ashleysommer avatar fmux avatar garyo avatar lanfon72 avatar thamenato 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

Watchers

 avatar  avatar  avatar  avatar

sanic-plugin-toolkit's Issues

0.8.2 change, new error

On the latest version, 0.8.2 I am getting caught with the error, App must be running before you can run middleware!, here, but not on previous releases. Using spf as a requirement of Sanic-Cors.

Output:

Starting...
[2019-08-20 07:44:33 -0400] [66363] [DEBUG]

                 Sanic
         Build Fast. Run Fast.


[2019-08-20 07:44:33 -0400] [66363] [INFO] Goin' Fast @ http://0.0.0.0:8000
[2019-08-20 07:44:43 -0400] [66363] [ERROR] Exception occurred while handling uri: 'http://127.0.0.1:8000/'
Traceback (most recent call last):
  File ".../venv/lib/python3.7/site-packages/sanic/app.py", line 910, in handle_request
    response = await self._run_request_middleware(request)
  File ".../venv/lib/python3.7/site-packages/spf/framework.py", line 533, in _run_request_middleware
    "App must be running before you can run middleware!"
AssertionError: App must be running before you can run middleware!
[2019-08-20 07:44:43 -0400] [66363] [DEBUG] CORS: Request to '/' matches CORS resource '/*'. Using options: {'origins': ['.*'], 'methods': 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT', 'allow_headers': ['.*'], 'expose_headers': None, 'supports_credentials': False, 'max_age': None, 'send_wildcard': False, 'automatic_options': False, 'vary_header': True, 'resources': '/*', 'intercept_exceptions': True, 'always_send': True}
[2019-08-20 07:44:43 -0400] [66363] [DEBUG] CORS: Cannot find the request context. Is request already finished?
[2019-08-20 07:44:43 -0400] [66363] [DEBUG] CORS: Cannot find the request context. Is request already finished?
[2019-08-20 07:44:43 -0400] - (sanic.access)[INFO][127.0.0.1:54246]: GET http://127.0.0.1:8000/  500 144
[2019-08-20 07:44:48 -0400] [66363] [DEBUG] KeepAlive Timeout. Closing connection.

Python 3.7.4 (but same error with earlier versions)

sanic==19.6.2
Sanic-Cors==0.9.8.post3
Sanic-Plugins-Framework==0.8.2

Test to reproduce error:

from sanic import Sanic
from sanic.response import json
from sanic_cors import CORS

import asyncio

app = Sanic(__name__)

CORS(app, resources=r'/*')

async def hello(request):
    return json({"hello": "world"})

app.add_route(
    hello,
    '/',
    methods=['GET', 'POST']
)

def run():
    loop = asyncio.get_event_loop()
    srv_coro = app.create_server(
        host='0.0.0.0',
        port=8000,
        return_asyncio_server=True,
        asyncio_server_kwargs=None,
        debug=True
    )
    asyncio.ensure_future(srv_coro)

    try:
        loop.run_forever()
    except KeyboardInterrupt:
        loop.stop()


if __name__ == '__main__':
    print('Starting...')
    run()

setup.py doesn't install plugins, only spf

Since SPF now comes with contextualize plugin, it would be good if setup.py installs it. Right now it only installs the 'spf' package.
However, it's probably best if the plugins package gets renamed to spf-plugins or put underneath spf, e.g. spf.plugins. I can do this, just let me know which way you prefer.

Incompatible with Sanic-21.12.0

Sanic 21.12.0 was release on 12/26/2021 and has breaking changes.

Specifically, it makes setting of attributes on Sanic or Blueprint instances an error where it was previously a DeprecationWarning. The plugin toolkit tries to wrap Sanic._startup but setting its value, which raises an AttributeError.

Here is a sample section that will raise the AttributeError. This line is reached because in 21.12 the Sanic.__fake_slots__ became Sanic.__slots__.

if hasattr(app, "_startup"):
# We can wrap startup, to patch _after_ Touchup is done
app._startup = update_wrapper(partial(self._startup, app, app._startup), app._startup)

Furthermore, Blueprint objects no longer have __fake_slots__ either, so this section also raises AttributeError

if SANIC_21_3_0 <= SANIC_VERSION:
_slots = list(Blueprint.__fake_slots__)
_slots.extend(["register"])
Sanic.__fake_slots__ = tuple(_slots)
bp.register = update_wrapper(partial(bp_register, bp, bp.register), bp.register)
setattr(bp.ctx, APP_CONFIG_INSTANCE_KEY, self)

Use proper semantic version

Hi,

Can you please also do release tags like 0.6.4 (without dev20181101 part)?

The thing is when I'm trying to install sanic-cors with pipenv it can't resolve SPF dependency.
There is a workaround to specify pipenv install sanic-cors --pre to resolve pre-release dependency, BUT this flag affects all other dependencies (for example it will coverage==5.0.3a, etc).

I saw previous ticket

Shared context loose entries

Env: Python 3.7.5, Sanic latest stable
I am trying to use contextualize to use the context.shared to track clients to track some values that should not be reinitialized pr request, this gets my avg request time down from 60000 ms to 15 ms, awesome feature :D

But I noticed entries dropping from the context now and again, is there a ttl for the dictionary keys and if so how to I override this? I need to do my own cleanup to keep data sane.

I been trying to reliably reproduce it and currently I cannot put my finger on what makes the values drop but I have a scenario where I have a Server Send Event (sse) stream sending data posted to other endpoints through context.shared, its working as a charm to a point, but when the client reconnects the sse client or drops the connection the shared context tend to drop after a short while.

First I thought it was because i got context.get('shared', None), but after porting to context.shared I still get the same problem. I am trying to use the global shared context for contextualize.

It would be great to get some feedback on the issue as I would love to get this up and running properly.

Websocket routes

Hello!

Thank you for a really nice framework! Really enjoying the context sharing features. One question:
is it possible to use websocket routes with SPF plugins so that context can be shared?

handling of arguments in SanicPlugin prevents multiple inheritance

The __init__() method of the SanicPlugin class checks for the presence of any arguments using the following lines (lines 429-437 in plugin.py at tag 0.8.2:

def __init__(self, *args, **kwargs):
    # Sometimes __init__ can be called twice.
    # Ignore it on subsequent times
    if self._initialized:
        return
    assert len(args) < 1,\
        "Unexpected arguments passed to this Sanic Plugins."
    assert len(kwargs) < 1,\
        "Unexpected keyword arguments passed to this Sanic Plugins."

However, in case of multiple inheritance, it may actually be necessary to pass on arguments to the next class in the method resolution order, as in the following proof-of-concept example:

from spf import SanicPlugin


class SecretKeyStore:
    def __init__(self, secret_key, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.secret_key = secret_key


class MyPlugin(SanicPlugin, SecretKeyStore):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


# secret key might be read from a plugin-specific config file
my_plugin = MyPlugin(secret_key="abcd1234")

Note that in this case it would still be possible to get the code to run by swapping the order of the base classes, however as soon as one has more than one base class that handles arguments the way SanicPlugin does, one is out of luck.

Is there any reason why the two assertions are necessary? If not, I believe they can be simply deleted. If the only remaining class in the method resolution order is object, it will complain anyway if there are any arguments that were not accounted for.

Add context example?

I'm trying to use SPF with the built-in Contextualize plugin to add context to my app; specifically I'd like to tie the Sqlalchemy database sessionmaker to the app so anywhere I have the app (I'd like to do this for requests too) I can create a db session. I looked at the Contextualize example but it wasn't clear to me how to add things to the app context (or maybe Contextualize doesn't do that?)

Incorrect package naming

When I run pip freeze with sanic-plugin-toolkit installed, it actually prints out sanic plugin toolkit==1.0.1 (notice the missing dashes).
Tested with python 3.8.10 and pip versions 21.2.3 and 21.2.4.

The output includes:

sanic==21.6.2
sanic plugin toolkit==1.0.1
Sanic-Cors==1.0.0
sanic-jwt==1.7.0
sanic-openapi==21.6.1
sanic-routing==0.7.1

__new__() missing 1 required positional argument: 'version_prefix'

From @jailge here: ashleysommer/sanic-restplus#27
Hi, I currently use python3.7, sanic 21.6.2 and sanic-restplus 0.6.0.
When I run my project, I got a type error as below:

File "/Users/jailge/PycharmProjects/eslogsystem/sanic-ls-service/venv/lib/python3.7/site-packages/sanic_plugin_toolkit/realm.py", line 350, in _plugin_register_app_route fr = SanicFutureRoute(r_handler, uri, name=name, **kwargs) TypeError: __new__() missing 1 required positional argument: 'version_prefix'

How to use with websocket blueprint?

I'm trying to use the contextualize plugin to add a db engine to my app as in the example, and I have the plugin loading and registered using the before_server_start listener.

@contextualize.listener('before_server_start')
async def setup_db(app, loop, context):
    from sqlalchemy import create_engine
    shared_context = context.shared
    engine = create_engine(app.config.get('DB_URI'))
    shared_context['db'] = engine
    dbsessionmaker = sessionmaker(bind=engine)
    shared_context['dbsessionmaker'] = dbsessionmaker
    logger.debug(f'SPF: setup_db: db is {engine}, dbsession_factory is {dbsessionmaker}')

def register_spf(app): # called during app setup
    spf = SanicPluginsFramework(app)
    spf.register_plugin(contextualize)

I'm not clear on how to use contextualize with a websocket blueprint like this (in another file):

ws_bp = Blueprint('websocket_api', url_prefix='/websock/v1')

# later...
@ws_bp.websocket('/')
async def websock(request, ws):
    logger.info(f'Server got websock connection {ws}')
    socket = ClientSocket(ws)
    # ...

Attributeerror: registration error occurred while registering the plug-in

The following error occurred when I registered the plug-in. I don't know where the problem is. I have written a plug-in using SPT. It runs normally, but now the error is very strange

Traceback (most recent call last):
  File "/usr/lib/python3.7/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/lib/python3.7/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/home/wang/.vscode/extensions/ms-python.python-2021.5.926500501/pythonFiles/lib/python/debugpy/__main__.py", line 45, in <module>
    cli.main()
  File "/home/wang/.vscode/extensions/ms-python.python-2021.5.926500501/pythonFiles/lib/python/debugpy/../debugpy/server/cli.py", line 444, in main
    run()
  File "/home/wang/.vscode/extensions/ms-python.python-2021.5.926500501/pythonFiles/lib/python/debugpy/../debugpy/server/cli.py", line 285, in run_file
    runpy.run_path(target_as_str, run_name=compat.force_str("__main__"))
  File "/usr/lib/python3.7/runpy.py", line 263, in run_path
    pkg_name=pkg_name, script_name=fname)
  File "/usr/lib/python3.7/runpy.py", line 96, in _run_module_code
    mod_name, mod_spec, pkg_name, script_name)
  File "/usr/lib/python3.7/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/home/wang/code/python/Test/srf-project-template/run.py", line 16, in <module>
    from apps import app
  File "/home/wang/code/python/Test/srf-project-template/apps/__init__.py", line 49, in <module>
    app = create_app()
  File "/home/wang/code/python/Test/srf-project-template/apps/__init__.py", line 41, in create_app
    realm.register_plugin(srf_cache)
  File "/home/wang/code/venv/dev_srf/lib/python3.7/site-packages/sanic_plugin_toolkit/realm.py", line 203, in register_plugin
    if plugin.is_registered_in_realm(self):
  File "/home/wang/code/venv/dev_srf/lib/python3.7/site-packages/sanic_plugin_toolkit/plugin.py", line 427, in is_registered_in_realm
    for reg in self.registrations:
AttributeError: registrations

code run.py

def create_app():
    """工厂函数"""
    sanic_app = Sanic(name='')
    sanic_app.update_config(Path.cwd() / cofing_name)
    realm = SanicPluginRealm(sanic_app)
    realm.register_plugin(cors) # ok
    realm.register_plugin(apps_helper) # ok 
    realm.register_plugin(srf_cache) # error
    register_tortoise(
        sanic_app, db_url=sanic_app.config.get('DB_CONNECT_STR'), modules=apps_helper.models
        # , generate_schemas=False
    )
    return sanic_app

code plug.py

class Cache(SanicPlugin, metaclass=SingletonMeta):
   
    def __init__(self, *args, **kwargs):
        self._caches = ObjectDict()
        self._current_cache = None

        self.connections = ObjectDict()
        self._external_connections = ObjectDict()

    def on_registered(self, context, reg, *args, **kwargs):
        self.init_app(context, *args, **kwargs)

    def get_config(self, app):
        app_config = app.config
        if CACGE_CONFIG_KEY not in app_config:
            raise TypeError("Sanic config not '%s' value" % CACGE_CONFIG_KEY)
        config = app_config.get(CACGE_CONFIG_KEY)
        if not isinstance(config, dict):
            raise TypeError("Sanic config '%s' value must be a dict type" % CACGE_CONFIG_KEY)
        return config

    def init_db(self, app, engines):

        @app.listener('before_server_start')
        async def aredis_configure(_app, loop):
            for name, config in engines.items():
                engine_module = config['engine_module']
                db_config = config['db_config']
                external = config['external']
                if not engine_module.has_db:
                    self._caches[name] = engine_module(connection)
                    continue
                connection = engine_module._create_connection(db_config)
                client_name = name.lower()
                self.connections[client_name] = connection
                self._caches[name] = engine_module(connection)
                if external:
                    self._external_connections[client_name] = connection
            setattr(_app, 'cache_db_conn', self._external_connections)

        @app.listener('after_server_stop')
        async def close_redis(_app, _loop):
            for engine in engines.values():
                engine._disconnect_connection()

    def init_app(self, context, *args, **kwargs):
        app = context.app
        self.config = self.get_config(app=app)
        engine_list = {}
        for name, config in self.config:
            engine_module_path = config.get('engine')
            engine_module = import_modul_by_str(engine_module_path)
            db_config = config.get('db_config')
            external = config.get('external')
            engine_list[name] = {
                'engine_module': engine_module,
                'db_config': db_config,
                'external': external

            }

        self.init_db(app, engine_list)

    def select(self, name):
        if name in self.all_caches:
            cache = self.all_caches[name]
            self.current_cache = cache
            return cache
        raise CacheNoFoundEXC()

    @property
    def current_cache(self):
        if self._current_cache is None:
            self._current_cache = self.all_caches
        return self._current_cache

    @property
    def all_caches(self):
        return self._caches

Pipenv dependancy resolution error

Hi,

I've been using sanic-cors via pipenv and I've been getting a dependancy resolution error. My thoughts are because the version identifiers contain the string "dev", thought I thought python version parsing did allow for that.

I've opened a main ticket here, but would appreciate any help resolving it.

Thanks

Deprecation of variables set on Sanic instances

Python version: 3.9.2
Sanic version: 21.3.2 (latest)

UserWarning: Setting variables on Sanic instances is deprecated and will be removed in version 21.9. You should change your Sanic instance to use instance.ctx.handle_request instead.
UserWarning: Setting variables on Sanic instances is deprecated and will be removed in version 21.9. You should change your Sanic instance to use instance.ctx._run_request_middleware instead.
UserWarning: Setting variables on Sanic instances is deprecated and will be removed in version 21.9. You should change your Sanic instance to use instance.ctx._run_response_middleware instead.

Memory leak in create_temporary_request_context() method

Hi @ashleysommer , it seems that there is a memory leak in the handling of the websocket requests. I am able to reproduce it with https://github.com/ashleysommer/sanic-cors

Long story short:

There's a call of SanicPluginsFramework.create_temporary_request_context in method SanicPluginsFramework._run_request_middleware which preserves a link to the current request in shared context. It seems that this temporary context is intended to be deleted in SanicPluginsFramework.delete_temporary_request_context, but it doesn't happen because method SanicPluginsFramework._run_response_middleware won't be called in case of websockets (response is None and response middleware won't be called as well https://github.com/huge-success/sanic/blob/master/sanic/app.py#L950).

How to reproduce:

  1. Download issue.zip
  2. Install sanic, sanic_cors, tqdm, aiohttp
  3. Run ws_server.py (in attached zip)
  4. Run ws_bench.py
  5. Look at chain.png in the same directory, there will be a reference graph with stalled reference to random CIMultiDict instance
  6. Commenting out SanicPluginsFramework.create_temporary_request_context fixes the issue

I briefly looked at the source code but didn't come up with any guess how to fix it. Any ideas?

Thanks a lot!

1.0.0 is not on pypi

Hello,

Last release available through PIP is 0.9.5
Is it purposefully ?

Damien

You have taken the `spf` import name already belonging to `pyspf`

Hi
i recently raised an issue (and contributed a PR) to the project pyspf since their module is imported as spf like yours and i need both

they replied with

This project has been using the spf module name since 2004. 
I think it would be ridiculous for us to rename because of a less 
than three year old project that obviously failed to evaluate
the name space before using it. 
...
Typically in the case of a namespace conflict, it's the newer 
project that renames. The structure of the package reflects it's age.

so what are we gonna do?
currently i cant import both and it makes me a sad panda 😭

here is the PR:
sdgathman/pyspf#20

and the issue is pyspf-module_name==sanicpluginsframework-package_name

kind regards
Johannes

Sanic incompatibility problem

sanic ==0.4.1
sanic-plugin-toolkit ==1.2.0

error:

ModuleNotFoundError: No module named 'sanic.models'

File "e:\venv\sanic_admin\lib\site-packages\sanic_plugin_toolkit\realm.py", line 17, in
from sanic.log import logger
ImportError: cannot import name 'logger' from 'sanic.log'

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.