Package: microse
🌹MicroseMicrose (stands forMicro Remote Object Serving Engine) is a light-weight
engine that provides applications the ability to serve modules as RPC services,
whether in another process or in another machine.This is the python version of the microse implementation. For API reference,
please check theAPI documentation, or theProtocol Reference.Other implementations:microse-nodeNode.js implementationmicrose-swoolePHP implementation
based on swooleInstallpipinstallmicrosePeel The OnionIn order to use microse, one must create a rootModuleProxyAppinstance, so
other files can use it as a root namespace and access its sub-modules.Example# app/app.pyfrommicrose.appimportModuleProxyAppimportos# Create an abstract class to be used for IDE intellisense:classAppInstance(ModuleProxyApp):pass# Create the instance amd add type annotation,# 'app' will be the root namespace for modulesapp:AppInstance=ModuleProxyApp("app")In other files, just define a class with the same name as the filename, so that
another file can access it directly via theappnamespace.# Be aware that the class name must correspond to the filename.# app/Bootstrap.pyclassBootstrap:definit(self):# ...Don't forget to augment types in theAppInstanceclass if you need IDE typing
support:fromapp.BootstrapimportBootstrapasiBootstrapclassAppInstance(ModuleProxyApp):@propertydefBootstrap(self)->iBootstrap:passAnd other files can access to the modules via the namespace:# index.pyfromappimportapp#  Accessing the module as a singleton and calling its function directly.app.Bootstrap.init()Non-class ModuleIf a module doesn't have a class with the same name as the filename, the module
it it self will be used instead.# app/config.pyasyncdefget(name:str):# some async operations...returnvalue# call the function directlyvalue=awaitapp.config.get("someKey")Remote ServiceThe above example accesses the module and calls the function in the current
process, but we can do more, we can serve the module as a remote service, and
calls its functions as remote procedures.ExampleFor example, if I want to serve a user service in a different process, I just
have to do this:# app/services/User.py# It is recommended not to define the '__init__' method or use a non-parameter# '__init__' method.classUser:__users=[{"firstName":"David","lastName":"Wood"},# ...]# Any method that will potentially be called remotely shall be async.asyncdefgetFullName(self,firstName:str):foruserinself.__users:ifuser["firstName"]==firstName:returnf"{user['firstName']}{user['lastName']}"# app/app.pyfromapp.services.UserimportUserasiUserclassiServices:@propertydefUser(self)->iUser:passclassAppInstance(ModuleProxyApp):@propertydefservices(self)->iService:pass# server.pyfromappimportappimportasyncioasyncdefserve():server=awaitapp.serve("ws://localhost:4000")awaitserver.register(app.services.User)print("Server started!")loop=asyncio.get_event_loop()loop.run_until_complete(serve())loop.run_forever()Just trypython server.pyand the service will be started immediately.And in the client-side code, connect to the service before using remote
functions.# client.pyfromappimportappimportasyncioasyncdefconnect():client=awaitapp.connect("ws://localhost:4000")# Once registered, all functions of the service module will be remotized.awaitclient.register(app.services.User)# Accessing the instance in local style but actually calling remote.fullName=awaitapp.services.User.getFullName("David")print(fullName)# David Woodasyncio.get_event_loop().run_until_complete(connect())NOTE: to ship a service in multiple server nodes, just create and connect to
multiple channels, and register the service to each of them, when calling remote
functions, microse will automatically calculate routes and redirect traffics to
them.NOTE: RPC calling will serialize (via JSON) all input and output data, those
data that cannot be serialized will be lost during transmission.Generator SupportWhen in the need of transferring large data, generator functions could be a
great help, unlike general functions, which may block network traffic when
transmitting large data since they send the data as a whole, generator functions,
on the other hand, will transfer the data piece by piece.# app/services/User.pyclassUser:__friends={"David":[{"firstName":"Albert","lastName":"Einstein"},{"firstName":"Nicola","lastName":"Tesla"},# ...],# ...}asyncdefgetFriendsOf(self,name:str):friends=self.__friends.get(name)iffriends:forfriendinfriends:yieldf"{friend['firstName'}{friend['lastName']}"# index.pyasyncdefhandle():generator=app.services.User.getFriendsOf("David")asyncfornameingenerator:print(name)# Albert Einstein# Nicola tesla# ...# The following usage gets the same result.generator2=app.services.User.getFriendsOf("David")whileTrue:try:name=awaitgenerator2.__anext__()print(name)# Albert Einstein# Nicola tesla# ...# When all data has been transferred, a StopAsyncIteration exception# will be raised.exceptStopAsyncIteration:breakasyncio.get_event_loop().run_until_complete(handle())Life Cycle SupportMicrose provides a way to support life cycle functions, if a service class has
aninit()method, it will be used for asynchronous initiation, and if the
class has adestroy()method, it will be used for asynchronous destruction.
With these feature, the service class can, for example, connect to a database
when starting the server and release the connection when the server shuts down.# app/services/User.pyclassuser:# Instead of using '__init__()', which is synchronous, we define an# asynchronous 'init()' method.asyncdefinit(self):# ...# Instead of using '__del__()', which is synchronous, we define an# asynchronous 'destroy()' method.asyncdefdestroy(self):# ...Standalone ClientMicrose also provides a way to be running as a client-only application, in this
case the client will not actually load any modules since there are no such files,
instead, it just map the module names so you can use them as usual.In the following example, we assume thatapp.services.userservice is served
by a Node.js program, and we can use it in our python program as usual.frommicrose.appimportModuleProxyAppapp=ModuleProxyApp("app",False)# pass the second argument Falseasyncdefhandle():client=awaitapp.connect("ws://localhost:4000")awaitclient.register(app.services.user)fullName=awaitapp.services.user.getFullName("David")print(fullName)# David Woodasyncio.get_event_loop().run_until_complete(handle())For client-only application, you may need to declare all abstract classes:classiUser:defgetFullName(self,name:str)->str:passclassiServices:@propertydefuser(self)->iUser:passclassAppInstance(ModuleProxyApp):@propertydefservices(self)->iServices:passProcess InteropThis implementation supports interop in the same process, that means, if it
detects that the target remote instance is served in the current process,
the function will always be called locally and prevent unnecessary network
traffic.