Package: asynction
AsynctionSocketIO python framework driven by theAsyncAPIspecification. Built on top ofFlask-SocketIO. Inspired byConnexion.The purpose of Asynction is to empower a specification first approach when developingSocketIOAPIs in Python. It guarantees that your API will work in accordance with its documentation.Disclaimer: Asynction is still at a beta stage. Extensive testing is recommended when using this library in production.FeaturesRegisters all event and error handlers that are referenced within the API specification.Provides out of the box validation on every Socket.IO interraction. In particular:Event validation (for both ingress and egress events), based on the specified message schemataHTTP request validation, upon connection, based on the channel binding schemata of each namespaceCallback validation, upon the ACK of a message, based on the messagex-ackschemataSecurityà laConnexion. Handles OAuth2 and HTTP based authentication.Generates HTML rendered docs, similar to the AsyncAPIplayground. The docs get served through theGET {base_path}/docsroute of the app.Mock server supportCLIA complete example can be foundhere(includes examples of both normal and mock server implementations).PrerequisitesPython 3.7 (or higher)Install$pipinstallasynctionWith mock server support:$pipinstallasynction[mock]With CLI support:$pipinstallasynction[cli]The CLI can also be installed via Homebrew:$brewtapdedoussis/tap$brewinstallasynctionUsage (basic example)Example event and error handler callables located at./my_api/handlers.py:# /user namespacedefuser_sign_up(data):logger.info("Signing up user...")emit("metrics","signup",namespace="/admin",broadcast=True,callback=cb)defuser_log_in(data):logger.info("Logging in user...")emit("metrics","login",namespace="/admin",broadcast=True,callback=cb)returnTrue# Ackdefuser_error(e):logger.error("Error:%s",e)# /admin namespacedefauthenticated_connect():token=request.args["token"]defadmin_error(e):logger.error("Admin error:%s",e)Example specification located at./docs/asyncapi.yaml:asyncapi:2.3.0info:title:User Account Serviceversion:1.0.0description:This service is in charge of processing user accountsservers:production:url:my-company.com/api/socket.io# Customizes the `path` kwarg that is fed into the `SocketIO` constructorprotocol:wsschannels:/user:# A channel is essentially a SocketIO namespacepublish:message:oneOf:# The oneOf Messages relationship expresses the supported events that a client may emit under the `/user` namespace-$ref:"#/components/messages/UserSignUp"-$ref:"#/components/messages/UserLogIn"x-handlers:# Default namespace handlers (such as connect, disconnect and error)error:my_api.handlers.user_error# Equivelant of: `@socketio.on_error("/user")`/admin:subscribe:message:oneOf:-"#/components/messages/Metrics"x-handlers:connect:my_api.handlers.authenticated_connect# Equivelant of: `@socketio.on("connect", namespace="/admin")`error:my_api.handlers.admin_errorbindings:# Bindings are used to validate the HTTP request upon connection$ref:"#/components/channelBindings/AuthenticatedWsBindings"components:messages:UserSignUp:name:sign up# The SocketIO event name. Use `message` or `json` for unnamed events.payload:# Asynction uses payload JSON Schemata for message validationtype:objectx-handler:my_api.handlers.user_sign_up# The handler that is to be registered. Equivelant of: `@socketio.on("sign up", namespace="/user")`UserLogIn:name:log inpayload:type:objectx-handler:my_api.handlers.user_log_inx-ack:# Specifies the structure of the ACK data that the client should expectargs:type:booleanMetrics:name:metricspayload:type:stringenum:[signup,login]x-ack:# Specifies the structure of the ACK data that the server expectsargs:type:stringchannelBindings:AuthenticatedWsBindings:ws:query:type:objectproperties:token:type:stringrequired:[token]Bootstrap the AsynctionSocketIO server:fromasynctionimportAsynctionSocketIOfromflaskimportFlaskflask_app=Flask(__name__)asio=AsynctionSocketIO.from_spec(spec_path="./docs/asyncapi.yaml",app=flask_app,message_queue="redis://localhost:6379",# or any other kwarg that the flask_socketio.SocketIO constructor accepts)if__name__=="__main__":asio.run(app=flask_app)TheAsynctionSocketIOclass extends theSocketIOclass of the Flask-SocketIO library.The aboveasioserver object has all the event and error handlers registered, and is ready to run.Validation of the message payloads, the channel bindings and the ack callbacks is also enabled by default.Without Asynction, one would need to add additional boilerplate to register the handlers (as shownhere) and implement the respective validators.Security (Authentication and Authorization)Asynction supports authentication of incoming connections through the security mechanisms specified in the AsyncAPI spec of an application. Seethis guideon how to add security as part of an API specification. To take advantage of this feature, a security handler callable should be attached to each security scheme definition under thecomponentssection. To attach a security handler(s), see thesecurity specification extentionsection below.The security handler callable(s) will be called upon every new client connection and MUST return aSecurityInfotyped dictionary (which allows extra keys). Asynction then validates this returned dictionary, refusing the connection to any unauthenticated/unauthorised requests. Finally, the validatedSecurityInfodictionary is passed to the connection handler as an extratoken_infokwarg, to allow further/custom processing if needed.DocsAPI documentation is autogenerated by Asynction and served through the following routes of the app:{base_path}/docs: Rendered HTML docs similar to the AsyncAPIplayground.{base_path}/docs/asyncapi.json: The raw specification data exposed for programmatic retrieval.Thebase_pathis determined automagically through the Socket.IO path argument. It essentially is the parent of that path. For example:Socket.IO pathBase pathDocs pathsocket.io(default)//docsevents/socket.io/events/events/docsDocs can be disabled by toggling thedocskwarg of theAsynctionSocketIO.from_specfactory method.Emitting from an external processAll validation features of Asynction can be used when emitting/sending events from an external process. See therelevant Flask-SocketIO documentation. Note that the SocketIO instance of the external process needs to be constructed using the sameAsynctionSocketIO.from_specfactory.Mock serverAsynction can also create a fake "mock" based off an AsyncAPI document. This enables the consumers of a SocketIO API to interract with the API before it's even built.fromasynctionimportMockAsynctionSocketIOfromflaskimportFlaskflask_app=Flask(__name__)mock_asio=MockAsynctionSocketIO.from_spec(spec_path="./docs/asyncapi.yaml",app=flask_app,)if__name__=="__main__":mock_asio.run(app=flask_app)The mock server:Listens for all events defined in the given spec, returning fake acknowledgements where applicable.Periodically emits events containing payloads of fake data, for the clients to listen on.The fake data generation is fueled byFakerandHypothesis, hence the use of the mock server functionality requires the installation of extra dependecies:pip install asynction[mock]To make the fake generated data more realistic, one may attach faker providers to the string schemata of their spec using theformatkeyword of JSON Schema:# example of a Message objectNewMessageReceived:name:new messagepayload:type:objectproperties:username:type:stringformat:first_namemessage:type:stringformat:sentencerequired:[username,message]The formats supported are essentially all thefaker providersthat yield a string value.CLIFor convenience, Asynction provides a command-line interface (CLI) that aims to be a toolbox of useful utilities for the development, testing and mocking of Asynction apps (ie any Socket.IO app driven by an AsyncAPI doc). For example, it allows one to run a "mock" instance of their Socket.IO server, only by passing the AsyncAPI YAML file, without even having to start the development of the server itself.All commands support the–-help(or-h) argument to display additional information.Available commandsmock run$asynction--spec./docs/asyncapi.ymlmockrun--port5001--debugger* Restarting with stat* Debugger is active!* Debugger PIN: 339-844-897(71320)wsgi starting up on http://0.0.0.0:5001...scaffold(coming soon)$asynction--spec./docs/asyncapi.ymlscaffold✨ Successfully generated app.pyDockerisedThe CLI can be installed via pip or Homebrew (see theinstall section) but is also available through docker, negating the need for a local python environment:$dockerrun-v${PWD}/docs/asyncapi.yml:/opt/asynction/asyncapi.ymldedoussis/asynctionmockrun--debugger* Restarting with stat* Debugger is active!* Debugger PIN: 339-844-897(71320)wsgi starting up on http://0.0.0.0:5000...Further resourcesAPI referenceComplete exampleSpecification ExtentionsAsynction has extended the AsyncAPI 2.x.x specification to provide support for coupling SocketIO semantical entities (such as namespaces, events and acks) to python objects (such as handler callabes or otherflask_socketio.SocketIOmethods). Some of the extentions below are necessary to express the Socket.IO protocol semantics, while others are solely needed for the programmatic purposes of Asynction. The extentions introduced adhere to theSpecification Extention guidelinesof the AsyncAPI spec.For further guidance on how to generally express a SocketIO API using AsyncAPI, refer to this article:https://dedouss.is/posts/2021-07-14-documenting-socketio-part-2.htmlEvent handlerThex-handlerfield MAY be defined as an additional property of theMessage Object. The value of this field MUST be ofstringtype, expressing a dot joint path to a python callable (the event handler).Message Objects listed under asubscribeoperationMUST include thex-handlerfield.Message Objects listed under apublishoperationSHOULD NOT include thex-handlerfield.Default namespace handlersThex-handlersfield MAY be defined as an additional property of theChannel Item Object. The value of this field SHOULD be aChannel Handlers Object.Channel Handlers ObjectField NameTypeDescriptionconnectstringDot joint path to the python connect handler callabledisconnectstringDot joint path to the python disconnect handler callableerrorstringDot joint path to the python error handler callableACK packetThe basic unit of information in theSocket.IO protocolis the packet. There are 7 distinctpacket types. ThepublishandsubscribeMessage Objects expressed in the A2S YAML above correspond to theEVENTandBINARY_EVENTpacket types. These are essentially the packets that are transmitted when the Socket.IO sender invokes theemitorsendAPI functions of the Socket.IO library (regardless of implementation). In turn, the Socket.IO event receiver handles the received event using theonAPI function of the Socket.IO library. As part of theonhandler, the receiver may choose to return an acknowledgement of the received message. This acknowledgement is conveyed back to the transmitter via theACKandBINARY_ACKpacket types. This ack data is passed as input into the callback that the message transmitter has provided through theemit/sendinvocation.In order to express the above acknowledgement semantics, the A2S specification needs to be extended as follows:Message Objects MAY include thex-ackfield. The value of this field SHOULD be aMessage Ack Object.Components ObjectMAY include thex-messageAcksfield. The value of this field should be of type:Map[string, Message Ack Object | Reference Object]Although Asynction uses these fields to validate the input args of the callback functions, these ACK extentions are necessary to express semantics of theSocket.IO protocol, regardless of any tooling used for automation / code generation.Message Ack ObjectField NameTypeDescriptionargsSchema ObjectSchema of the arguments that are passed as input to the acknowledgement callback function. In the case of multiple arguments, use thearraytype to express the tuple.In the future, the Message Ack Object may be extended with extra fields to enable additional documentation of the callback.Security handersIn order to support theAuthN/AuthZ functionalityof asynction, theSecurity Scheme Objectneeds to be extended as follows:A Security Scheme Object ofoauth2type MUST include thex-tokenInfoFuncfield.A Security Scheme Object ofoauth2type MAY include thex-scopeValidateFuncfield.A Security Scheme Object of basichttptype MUST include thex-basicInfoFuncfield.A Security Scheme Object of bearerhttptype MUST include thex-basicBearerInfoFuncfield.A Security Scheme Object of bearerapiKeytype MUST include thex-apiKeyInfoFuncfield.The value of all these fields MUST be ofstringtype, expressing a dot joint path to a python callable (the security handler).Per Namespace SecurityThex-securityfield MAY be defined as an additional property of theChannel Item Object.
The value of this field MUST be anarrayofSecurity Requirement Objectswhich is the same format used to specifyServer Security Requirements.
If a namespace specifiesx-securitythe security requirements specified for that namespace will overwrite any security requirements specified in theServer Object.