Base classes
We have some base classes for mainly creating factories with instances of certain types.
Base
Base is the type where any configuration can be defined:
from jumpscale.core.base import Base, fields
class Address(Base):
x = fields.Integer()
name = fields.String()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.x = 123
class Wallet(Base):
ID = fields.Integer()
addresses = fields.Factory(Address)
class User(Base):
wallets = fields.Factory(Wallet)
user = User()
w = user.wallets.get("aa")
addr1 = w.addresses.new("mine")
addr1.x = 456
addr2 = w.addresses.new("another")
addr2.x = 680
w.addresses.delete("another")
Note that, these configuration is not yet stored or saved, you need to use a stored factory with this Base
type.
Overriding constructor for Base sub-classes
When you override the consturctor of Base
sub-classes, you need to pass all arguments to super Base
, you can simply use super().__init__(*args, **kwargs)
, like:
class Address(Base):
x = fields.Integer()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.x = 123
Stored factory
The backend to store configurations of Base
types where can create, list and delete instances.
- Support encryption via secret fields (
fields.Secret
) - Multiple storage backends (FileSystemStore, RedisStore)
Example:
class Address(Base):
x = fields.Integer()
name = fields.String()
def __init__(self):
super().__init__()
self.x = 123
class Wallet(Base):
ID = fields.Integer()
addresses = fields.Factory(Address)
class User(Base):
name = fields.String()
wallets = fields.Factory(Wallet)
users = StoredFactory(User)
user1 = users.get("user1")
print(user1.instance_name)
user1.name = "ahmed"
user1.save()
Fields
Fields of the config can have default value, required or optional, indexed or not, set of validators as well
See a complete list of available fields at https://threefoldtech.github.io/js-ng/api/jumpscale/core/base/fields.html.
Clients
Clients can be defined the same as any Base class, they're a special type of Base
"""
Redis client
"""
from redis import Redis
from jumpscale.core import events
from jumpscale.clients.base import Client
from jumpscale.core.base import fields
from jumpscale.core.base.events import AttributeUpdateEvent
class RedisClientAttributeUpdated(AttributeUpdateEvent):
pass
class RedisClient(Client):
hostname = fields.String(default="localhost")
port = fields.Integer(default=6379)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__client = None
def _attr_updated(self, name, value):
super()._attr_updated(name, value)
# this will allow other people to listen to this event too
event = RedisClientAttributeUpdated(self, name, value)
events.notify(event)
# reset client
self.__client = None
def __dir__(self):
return list(self.__dict__.keys()) + dir(self.redis_client)
@property
def redis_client(self):
if not self.__client:
self.__client = Redis(self.hostname, self.port)
return self.__client
def __getattr__(self, k):
# forward non found attrs to self.redis_client
return getattr(self.redis_client, k)
Then we can simply create a stored factory for this client as:
redis = StoredFactory(RedisClient)
Data updates and computed fields
In your instance, you can handle data updates in many ways, from triggering a handler on single field updates, to event-based system.
The following are three different ways to do it:
Single field updates
If you need to do an action only when a single field is changed, You can use on_update
for any field, which should be a callable that takes the instance and new value, see stellar client as an example:
class Stellar(Client):
network = fields.Enum(Network)
address = fields.String()
def secret_updated(self, value):
self.address = stellar_sdk.Keypair.from_secret(value).public_key
secret = fields.String(on_update=secret_updated)
In this code, we set a new value for the address in case the secret is updated.
Computed and non-stored fields
Sometimes you need a field to be computed from other multiple fields, in such case, you just need to provide a compute
function which takes current instance and it should return the computed value, see the following example:
class User(Base):
emails = fields.List(fields.String())
first_name = fields.String(default="")
last_name = fields.String(default="")
def get_full_name(self):
name = self.first_name
if self.last_name:
name += " " + self.last_name
return name
def get_unique_name(self):
return self.full_name.replace(" ", "") + ".user"
full_name = fields.String(compute=get_full_name)
unique_name = fields.String(compute=get_unique_name)
users = StoredFactory(User)
user1 = users.get("test1")
print(user1.full_name) #=> "ahmed mohamed"
print(user1.unique_name) #=> "ahmedmohaed.user"
user1.first_name = "x"
user1.last_name = "y"
user1.save()
Note that, when saving the user object with this factory, the computed field will be saved too.
In other cases, you need to create a non-stored computed fields, which also do not need any serialization, but only to be created and used at run-time, this can be done by passing stored=False
to this field (which is True
by default):
class Greeter:
def __init__(self, name):
self.name = name
def say(self):
print("hello", self.name)
class User(Base):
first_name = fields.String(default="")
last_name = fields.String(default="")
def get_full_name(self):
name = self.first_name
if self.last_name:
name += " " + self.last_name
return name
full_name = fields.String(compute=get_full_name)
def get_my_greeter(self):
return Greeter(self.full_name)
my_greeter = fields.Typed(Greeter, stored=False, compute=get_my_greeter)
ahmed_greeter = fields.Typed(Greeter, stored=False, default=Greeter("ahmed"))
users = StoredFactory(User)
user = users.get("test1")
user.first_name = "abdo"
user.last_name = "tester"
user.my_greeter.say() #=> "hello abdo tester"
user.ahmed_greeter.say() => "hello ahmed"
user.save()
we created two Typed
fields of Greeter
class, and used them without any problems, when saving this instance, these fields won't be serialized nor stored.
Attribute updates and events
A more advanced feature are events and _attr_updated
method, in any instance, you can override _attr_updated
to handle attribute updates, also by default, an event of the type AttributeUpdateEvent
is fired.
Using events can allow other components to handle events related to your base implementation, so it's not needed unless you need other people to know about your changes.
In this way, the instance receives a call or an AttributeUpdateEvent
for any attribute.
An example of using attr_updated
or events to re-create an inner client (redis), as the client need to re-created if any of hostname
or port
has been changed:
Using _attr_updated
:
class RedisClient(Client):
hostname = fields.String(default="localhost")
port = fields.Integer(default=6379)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = Redis(self.hostname, self.port)
def _attr_updated(self, name, value):
# reset to get a new client
if name == 'hostname':
self.client = Redis(value, self.port)
elif name == 'port':
self.client = Redis(self.hostname, value)
Or using events (so, others can know of this change too):
class RedisClientAttributeUpdated(AttributeUpdateEvent):
pass
class RedisClient(Client):
hostname = fields.String(default="localhost")
port = fields.Integer(default=6379)
def __init__(self):
self.__client = None
super().__init__()
@property
def client(self):
if not self.__client:
self.__client = Redis(self.hostname, self.port)
return self.__client
def _attr_updated(self, name, value):
# calling super()._attr_updated is optional, as it will also notify
# with an event type of AttributeUpdateEvent
# super()._attr_updated(name, value)
# this will allow other people to listen to this event too
event = RedisClientAttributeUpdated(self, name, value)
events.notify(event)
# reset client
self.__client = None
class ImplThatDependsOnRedisClientChange(events.Handler, Base):
def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs):
# listen to this event
events.add_listenter(self, RedisClientAttributeUpdated)
def handle(self, ev):
# do something with ev.instance...etc
We created our own custom event type of RedisClientAttributeUpdated
.
In case of any changes, we notify others that the client attribute changed, and also, handle this change too, by using events.add_listenter(self, RedisClientAttributeUpdated)
.