Open oberstet opened 6 years ago
May I suggest Flask Blueprints API design?
Flask, basically, splits Component
into two parts: the top-level server (flask.Flask
) and module-level components (flask.Blueprint
). This allows developers to build modular components exposing blueprints and register all of them in the application root, thus leaving the configuration up to the application. Here is how I would love to use Autobahn:
# FILE: app/modules/demo/__init__.py
from autobahn.asyncio.component import Component
demo_component = Component(common_prefix='com.example.demo.')
# NOTE: the function gets automatically registered with
# `com.example.demo.random` id (i.e. `common_prefix` + function name)
@demo_component.register
async def random(*args, **kwargs):
return 42
# FILE: app/modules/__init__.py
from . import demo
components = [
demo.demo_component,
]
# FILE: app/__init__.py
from .modules import components
def create_app():
app = autobahn.Server(
'ws://127.0.0.1:8080/ws',
authentication={...},
...
)
for component in components:
app.register_component(component)
return app
if __name__ == '__main__':
app = create_app()
app.run()
@frol Some of the above registration stuff you can "kind-of" accomplish in certain ways .. I do like the idea of "an API-provider that can be registered at a WAMP prefix". I find myself wanting this very often. I think the Component
API in WAMP already embodies the concept of "connecting to a place, and authorization plus possible re-connection" .. which is what I think you mean with Server
above?
So perhaps what's wanted (instead of changing / expanding what Component
abstracts) is a new thing that knows about "an API" and can register it (optionally at a prefix); then it can be paired with a Component
(or even just a Session
) to register that API (i.e. uses Component.register
or Session.register
to hook up the methods). I could imagine, for example, a third-party library that wants to provide optional Autobahn support could then implement one of these new things. Ideally maybe it also knows about the concept of "readiness" (i.e. "is this API ready to go") and can publish that fact. Perhaps it also knows about dependencies (i.e. "this API needs APIs A and B to be ready first").
(Perhaps this discussion should move to some related-but-new ticket?) One thing the @demo_component.register
sketch above misses is, "what about publishing?". So if random
wants to publish something, it needs a currently-valid session.
Here is something similar, which should function right now:
# appfoo/login.py
import lmdb
from autobahn import wamp
from autobahn.wamp.types import Deny
async def on_join(session, details):
env, db = await _create_db_connection()
login = _ApplicationLogin(env, db, session)
# if required, could session.on('leave', cleanup) for example
await session.register(login, prefix="com.appfoo.auth.") # like "register_component")
session.publish("com.appfoo.auth.ready", True)
return
async def _create_db_connection():
env = lmdb.open(
path='...',
max_dbs=16,
)
db = env.open_db(b'auth_db')
return env, db
class _ApplicationLogin(object):
def __init__(self, env, db, session):
self.env = env
self.db = db
self.session = session
async def _get_pubkey_and_role(self, authid):
with lmdb.Transaction(env, db) as txn:
data = txn.get(pubkey.encode('ascii'))
if data is None:
raise KeyError("No such authid '{}'".format(authid))
user = json.loads(data.decode('utf8'))
return (user['pubkey'], user['role'])
# this will end up at "com.appfoo.auth.authenticate"
@wamp.register(None)
async def authenticate(self, realm, authid, extra):
try:
pubkey, role = await self._get_pubkey_and_role(authid)
except KeyError:
return Deny()
return {
"role": role,
"pubkey": pubkey,
}
As a bonus, this lets you hook up "a valid session" (and, for example, register this thing on multiple components / session). It also handles dis- and re-connect well, because the "on_join" handler will run every time we re-connect to the router. There are two ways to hook this up; in code you could do this:
comp = Component(...)
from appfoo import login
comp.on('join', login.on_join)
If using crossbar, you can add a function
type component in the config:
{
"type": "function",
"callbacks": {
"join": "appfoo.login.on_join"
},
"realm": "com.example.authenticator",
"role": "auth"
},
Perhaps there's a way to abstract this a little more and also possibly avoid some boilerplate?
This issue is to finally move the new **WAMP Component API ("new API") into "first class support" stage.
Eg here is an example https://github.com/crossbario/autobahn-python/blob/master/examples/twisted/wamp/component/frontend_scram.py
Things that come to mind for this:
The issues that have been opened over the time where we discussed various approaches to "new API", until we settled on the component API: