systemPY

Python application component initialization system
The problem
The regular application contains many atomic components. Asyncio makes their initializing a little bit complicated. It's OK, when you have a single entrypoint and initialize your application components via your framework. While you add new components to your application iteratively, you don't see any problem
When you create any new entrypoint, you have to think a lot, how to initialize application components again, which callbacks should be called and in which order. But it's a half of the problem! You have to implement also graceful shutdown
The most painful part is one-time scripts. It's kind of The Banana Gorilla Problem: you just want a banana but you have to initialize a gorilla holding the banana and the entire jungle, and then gracefully shutdown it
Solution
This library allows you to implement application startup and shutdown in a
declarative way. You have to implement a class for each your component,
write the startup and shutdown code. Then you have to combine required
components as mixins into the current application App class. Then create an
instance and pass dependencies as keyword arguments. In case it's a self-hosted
app you have to call the instance.run_sync() method
Note that systempy is NOT a di framework, but it may be used with any of
them. Also systempy is NOT a binding to systemd, but I was inspired by it
and systempy is doing similar things on a much smaller scale
It's possible to use systemPY in three scenarios:
-
Secondary application, which is handled by another application like celery or starlette
-
Primary application, handles other applications. Such as Gunicorn/Uvicorn/... or Celery
Basic principles
There are 6 the most significant stages of the application lifecycle. Keep in mind that we also need to be able do a safe application reload. Just look at the code:
from systempy import (
DIRECTION,
TargetMeta,
register_target,
register_target_method,
)
class Target(metaclass=TargetMeta):
@register_target_method(DIRECTION.FORWARD)
def on_init(self) -> None: ...
@register_target_method(DIRECTION.FORWARD)
def pre_startup(self) -> None: ...
@register_target_method(DIRECTION.FORWARD)
async def on_startup(self) -> None: ...
@register_target_method(DIRECTION.BACKWARD)
async def on_shutdown(self) -> None: ...
@register_target_method(DIRECTION.BACKWARD)
def post_shutdown(self) -> None: ...
@register_target_method(DIRECTION.BACKWARD)
def on_exit(self) -> None: ...
-
on_initis called exactly once on the application startup -
pre_startupis called before the event loop is started -
on_startupis called exactly when event loop has started -
on_shutdownis called when the application is going to shutdown or reload but the event loop is still working -
post_shutdownis called after event loop has stopped or drained. When application is going to reload, next it would be calledpre_startup -
on_exitis called exactly once when application is going to stop
Target idea is similar to systemd's targets. Keep in mind such examples
like graphical.target or multi-user.target. It means that we have to
reach all pre-required targets to achieve this target
Systemd's Units are bound to target. Target is a reason of Unit
execution
Now about systemPY. To bind your component Unit to Target, you have
to subclass this Target. After subclassing the Target your IDE will
prompt you in defining your Unit component's methods it's just
overriding Target's methods. It's similar to abc.ABC, but everything is
optional.
The last but not least is the register_target_method. It defines the type
of this method and execution order for overriding of this method in
subclassed Unit components. When you define a synchronous method,
overriding it by an asynchronous method will cause an error
Funny fact: mypy would cause the warning if you override asynchronous method with a synchronous one. But it's a false-positive warning and this code will work. As mypy causes a warning here, I think nobody will use this feature
Payload execution order may be DIRECTION.FORWARD, DIRECTION.BACKWARD and
DIRECTION.GATHER. Typically you should use DIRECTION.FORWARD on
initialization and DIRECTION.BACKWARD on shutdown
Also you may use DIRECTION.GATHER direction. Registered callbacks will be
handled by asyncio.gather and will be executed in arbitrary order. You are
able to use here both synchronous and asynchronous methods
Also there are available register_hook_before and register_hook_after.
Use them to extend existing Targets. Please have a look in the
Target section for more information
Naming and roles
All the magic happens in TargetMeta metaclass. The TargetMeta is a subclass
of abc.ABCMeta, that's why you are able to to use @abc.abstractmethod
decorator
There are 6 roles of classes I found:
-
Target the interface which defines lifecycle methods -
Unit component with lifecycle methods -
Mixin class without lifecycle methods. It's special optimization ofTargetrole -
App the final "baked" class with composed lifecycle methods. Sincesystempy>=0.1.7it's allowed to subclass them too -
Builtins a special optimization to force skippingbuiltinsclasses processing bylibsystempy. Normally you wouldn't face it -
Metaclass the same kind of optimization asBuiltins, but used byTargetMetaand its subclasses. It happens automatically in the__init_subclass__hook
TargetMeta checks role kwarg. If kwarg role is not defined,
TargetMeta tries to parse class name and decide what to do
Here we are trying to manipulate class roles by class names. It's very similar to the idea of tailwind-css. You don't have to do any extra import, just follow class naming rules and be happy:
-
Classes with names, ends with
TargetorTargetABC/ matchesr'(\S*)Target(ABC)?$', will be interpreted as aTargetrole -
Classes with names, ends with
UnitorUnitABC/ matchesr'(\S*)Unit(ABC)?$', will be interpreted as anUnitrole -
Classes with names, ends with
MixinorMixinABC/ matchesr'(\S*)Mixin(ABC)?$', will be interpreted as aMixinrole. Remember: theMixinrole is a special optimization of theTargetrole which means that the class does not have own lifecycle methods -
Classes with names, ends with
App/ matchesr'(\S*)App$', will be interpreted as anApprole. Due App role does not allow subclassing, AppABC has no sense
from systempy import Target
class MyTarget(Target): ... # OK
class TargetMy(Target): ... # ERROR
class MyMixin(Target): ... # OK
class MixinMy(Target): ... # ERROR
class MyMixinABC(Target): ... # OK
class MixinABCMy(Target): ... # ERROR
class MyUnit(Target): ... # OK
class UnitMy(Target): ... # ERROR
class MyApp(MyUnit): ... # OK
class MyApplication(MyUnit): ... # ERROR
Sometimes you may prefer to pass to the class the explicit role. You can
find such examples in systempy code base too. When you are passing the
role kwarg, systempy doesn't try to parse class name:
That's all? Nope, it's the very beginning!
You are able to register own Target with your own lifecycle methods. The first
such example is already included:
from systempy import (
DIRECTION,
Target,
register_hook_after,
register_hook_before,
)
class ExtTarget(Target):
@register_hook_after(Target.on_startup, DIRECTION.FORWARD)
async def post_startup(self) -> None: ...
@register_hook_before(Target.on_shutdown, DIRECTION.BACKWARD)
async def pre_shutdown(self) -> None: ...
Here two new lifecycle methods there were registered:
-
post_startupcallbacks will be called exactly after finishedTarget.on_startupin aDIRECTION.FORWARDorder -
pre_shutdowncallbacks will be called before runningTarget.on_shutdownin aDIRECTION.BACKWARDorder
You are able to define your own lifecycle stages without any limit by binding
them before or after already existing. It's like systemd's Unit options
Before and After. Yes, systemPY is a small systemd's brother
You can find more examples. Actually the whole systempy itself is the example
of libsystempy usage. Don't be afraid to read systempy's source code
Method Resolve Order
I'll explain on the part of REPL example:
class MyReplApp( # INIT # SHUTDOWN
ConfigUnit, # 1 # 6
LoggerUnit, # 2 # 5
MyFirstDBUnit, # 3 # 4
RedisUnit, # 4 # 3
PTReplUnit, # 5 # 2
): ... # 6 # 1
Important: while you are implementing your Unit mixins, remember NEVER
call super() in lifecycle methods. These methods will be collected and called
by systemPY in the right order
Installing
Install systemPY from PyPI OR
from github repository: