Components¶
Opslib defines infrastructure in terms of Components
. They encapsulate a particular slice of
infrastructure, and are typically built from smaller components. They are
reusable, and configured via Props
.
Build¶
A typical component will have a build()
method which defines its structure:
from opslib.components import Component, Stack
from opslib.places import LocalHost
class Cat(Component):
def build(self):
self.speak = LocalHost().command(
args=["echo", "meow"],
)
class House(Component):
def build(self):
self.spot = Cat()
self.oscar = Cat()
stack = Stack()
stack.apartment = House()
By setting self.spot
and self.oscar
, we attach the Cat
instances to
the House
instance, thus adding them to our stack. The Cat
instances
take their names from the attribute names: spot
and oscar
.
Components know their place in the stack. For instance, calling str()
or print()
on them yields their full path. Calling repr()
also yields the class name:
>>> print(stack.apartment.spot)
apartment.spot
>>> print(repr(stack.apartment.spot))
<Cat apartment.spot>
Components can also enumerate their children if we iterate over them:
>>> print(list(stack))
[<House apartment>]
>>> print(list(stack.apartment))
[<Cat apartment.spot>, <Cat apartment.oscar>]
The build()
method is actually called on child components as a result of
attaching them to their parent. The exception is the
Stack
class; its build()
method gets called
during __init__
.
Props¶
A component expects its configuration to be supplied via named Props
.
from opslib.components import Component
from opslib.places import LocalHost
from opslib.props import Prop
class Cat(Component):
class Props:
color = Prop(str)
energy = Prop(int, default=2)
The Cat
component above expects a color
prop, which must be a string,
and an integer energy
prop, which, if missing, defaults to 2
.
Consuming props¶
When the component is instantiated, its keyword arguments are turned into
props, and set as self.props
:
class Cat(Component):
class Props:
color = Prop(str)
energy = Prop(int, default=2)
def build(self):
if self.props.energy > 5:
self.play = LocalHost().command(
args=["echo", f"You see a blur of {self.props.color}."],
)
>>> stack = Stack()
>>> stack.spot = Cat(color="orange", energy=11)
>>> print(stack.spot.props)
<InstanceProps: {'color': 'orange', 'energy': 11}>
>>> print(stack.spot.play.run().output)
You see a blur of orange.
>>> stack.oscar = Cat(color="orange")
>>> print(stack.oscar.props)
<InstanceProps: {'color': 'orange', 'energy': 2}>
>>> print(stack.oscar.play.run().output)
AttributeError: 'Cat' object has no attribute 'play'
Oscar doesn’t have the play
attribute because he’s too sleepy.
Lazy values¶
Sometimes a value is not available when a component is defined. It might depend
on another component that will be defined later, or on remote state. The
Lazy
class wraps such values, and the
evaluate()
function unwraps them. Multiple calls of
evaluate
on the same lazy value will result in a single evaluation; the
result is cached.
from opslib.lazy import Lazy, evaluate
def get_value():
print("get_value was called")
return "meow"
print("Preparing a lazy value")
cat = Lazy(get_value)
print("Evaluating ...")
value = evaluate(cat)
print("Value is", value)
This should output:
Preparing a lazy value
Evaluating ...
get_value was called
Value is meow
Component props will accept lazy values if they are defined with lazy=True
.
If so, the lazy object is wrapped again, and its type is checked when it’s
evaluated.