Implementing SiLA features¶
For each feature that is added to a SiLA Server/Client package, a file [featureidentifier]_impl.py
is generated in the feature_implementations
directory.
It contains a class [FeatureIdentifier]Impl
, which extends the generated base class [FeatureIdentifier]Base
.
This base class extends the class FeatureImplementationBase
.
All code examples follow the features implemented in the example server.
Unobservable property¶
For each unobservable property, a method get_[PropertyIdentifier](self, *, metadata)
is generated.
It is called when a SiLA Client requests the current property value.
# GreetingProvider.StartYear
def get_StartYear(self, *, metadata) -> int:
return 2022
Unobservable command¶
For each unobservable command, a method [CommandIdentifier](self, Param1, Param2, ..., *, metadata)
is generated.
It is called when a SiLA Client requests the execution of this command.
Each parameter from the feature definition is translated into one method argument (e.g. Param1
).
You have to return a [CommandIdentifier]_Responses
object, which is a NamedTuple
instance with one field per command response specified in the feature definition.
Example: The command SayHello has one response “Greeting”.
To implement this method, use return SayHello_Responses(Greeting=...)
.
# GreetingProvider.SayHello
def SayHello(self, Name: str, *, metadata) -> SayHello_Responses:
return SayHello_Responses(Greeting="Hello " + Name)
Observable property¶
When a SiLA Client requests to subscribe to an observable property, the method [PropertyIdentifier]_on_subscription(self, *, metadata)
is called.
It is defined in the Base
class. If you want to specify what happens on subscriptions, override this method, and don’t forget to call the super method:
# TimerProvider.CurrentTime: print a message when a client subscribes
def CurrentTime_on_subscription(self, *, metadata) -> None:
print("A client subscribed to MyProperty!")
super().MyProperty_on_subscription(metadata=metadata)
To update the property value, use the method update_MyProperty(new_value)
, which is also defined in the Base
class.
When this method is called, the new value is sent to all currently subscribed clients, and it is stored and sent immediately when a clients subscribes to the property.
You can call this method from anywhere, e.g. in the __init__
method of the class to set an initial value, and when other methods are called that change the value of the property.
If you regularly want to update the value, you can use the method FeatureImplementationBase.run_periocially()
.
from datetime import datetime, time, timezone
# TimerProvider.CurrentTime: update observable property every second
class TimerProviderImpl(TimerProviderBase):
def __init__(self, parent_server: Server) -> None:
super().__init__(parent_server=parent_server)
def update_current_time() -> None:
# get current time
current_time: time = datetime.now(tz=timezone.utc).timetz()
# update observable property
self.update_CurrentTime(current_time)
self.run_periodically(update_current_time, delay_seconds=1)
Observable command¶
Observable commands are implemented similarly to unobservable commands.
A method [CommandIdentifier](self, Param1, Param2, ..., *, metadata, instance)
is generated, which is called when a SiLA Client requests to execute the command.
Like command responses, intermediate responses are also represented by :py:class:~typing.NamedTuple` classes.
There are two additional things to keep in mind:
Command instance¶
The implementation method receives the additional parameter instance
, which is an ObservableCommandInstance
object.
It can be used to send updates to the client while the command is running
(progress
,
estimated_remaining_time
,
lifetime_of_execution
,
and send_intermediate_response()
).
Lifetime of execution¶
Instances of observable commands have a “lifetime of execution”. After its lifetime, client cannot subscribe to the status or intermediate responses of the instance. The lifetime can be extended, but not reduced.
Warning
When the lifetime is None
, it is infinite. This means that the command instance is stored in the server memory
until the server is shut down.
class TimerProviderImpl(TimerProviderBase):
def __init__(self, parent_server: Server) -> None:
...
self.Countdown_default_lifetime_of_execution = datetime.timedelta(seconds=2)
def Countdown(
self, N: int, Message: str, *, metadata, instance: ObservableCommandInstanceWithIntermediateResponses[Countdown_IntermediateResponses]
) -> Countdown_Responses:
# change status from `waiting` to `running`
instance.begin_execution()
instance.progress = 0
instance.estimated_remaining_time = datetime.timedelta(seconds=N)
instance.lifetime_of_exection = datetime.timedelta(2 + N)
for i in range(N, 0, -1):
instance.send_intermediate_response(Countdown_IntermediateResponses(i)) # send intermediate responses
instance.progress = (N - i) / N * 100
instance.estimated_remaining_time = datetime.timedelta(seconds=i)
time.sleep(1)
return Countdown_Responses(Message, datetime.datetime.now(tz=datetime.timezone.utc))
Error handling¶
Defined Execution Errors¶
If the feature definition contains defined execution errors, DefinedExecutionError
subclasses are generated.
You can import them from the generated feature submodule and raise them like normal Python exceptions.
from ..generated.timerprovider import CountdownTooLong
class TimerProviderImpl(TimerProviderBase):
def Countdown(self, N: int, ...):
if N > 9000:
raise CountdownTooLong
Undefined Execution Errors¶
If your code raises an uncaught exception, the SDK will wrap it in an UndefinedExecutionError
and sent that to the client.
Validation Errors¶
These are raised by the SDK automatically if a client sends parameters that violate constraints specified in the feature definition. In such cases, your feature implementation code will not be called.
Metadata¶
Receiving metadata in command/property implementations¶
The property access/subscription and also the command methods have a parameter metadata
, which is a MetadataDict
.
When SiLA clients send metadata along with a request for property access or command execution, this metadata will be included in that dict
-like object.
The keys are sila2.framework.Metadata
objects and the values are the metadata values sent by the client.
Metadata Interceptors¶
Usually, metadata affects multiple commands and properties and should be handled in the same way.
This can be achieved by implementing a MetadataInterceptor
and adding it to the Server
class.
# new file feature_implementations/delay_interceptor.py
from typing import Any, Dict
from sila2.framework import FullyQualifiedIdentifier
from sila2.server import MetadataDict, MetadataInterceptor
from ..generated.delayprovider import DelayProviderFeature, DelayTooLong
class DelayInterceptor(MetadataInterceptor):
def __init__(self):
# this interceptor affects the Delay metadata in the DelayProvider feature
super()__init__(affected_metadata=[DelayProviderFeature["Delay"]])
def intercept(self, parameters, metadata: MetadataDict, target_call: FullyQualifiedIdentifier) -> None:
delay: int = metadata[DelayProviderFeature["Delay"]]
if delay > 10_000:
raise DelayTooLong("Maximum delay is 10 seconds") # defined execution error
time.sleep(delay/1000) # delay is given in milliseconds
# in server.py
from .feature_implementations.delay_importceptor import DelayInterceptor
class Server(SilaServer):
def __init__(...):
...
# add interceptor to server
self.add_metadata_interceptor(DelayInterceptor())
start()
and stop()
: Setup and Shutdown¶
If you want to execute code on server startup or shutdown, you can override the
start()
and
stop()
methods
inherited from FeatureImplementationBase
.
These methods are called by the server, do not call them yourself.
Make sure to call the super methods:
def start(self) -> None:
print("Feature implementation started")
super().start()
def stop(self) -> None:
print("Feature implementation stopped")
super().stop()