Defining Your Own Classes¶
Hikaru supports the creation and integration of custom classes for a number of use cases. It can allow you to supply a custom class to an existing operation as well as creating instances of your custom class automatically when needed. Some use cases are quite simple to implement, while others demand a bit more knowledge of Python dataclasses to use properly. This section covers each of these use cases so that you can successfully create your own custom classes for use with Hikaru.
First Things First¶
Regardless of your use case, there are a few basic requirements that you need to meet in order for your classes to work properly within Hikaru:
First, the module that contains your custom classes must include a wildcard import of of all the classes in the model module upon which your custom class is based. So for example, if you wanted to create a subclass of
Pod
, you will run into errors if the module that contains yourPod
subclass has an import statement like the following:from hikaru.model.rel_1_26.v1 import Pod
Instead, you must import all of the symbols from that release/version:
from hikaru.model.rel_1_26.v1 import *
All of your custom classes must be defined at the top-level within your module, otherwise Hikaru won’t be able to find them when needed.
All of your custom classes must be a subclass of either HikaruDocumentBase (in the case of top-level classes that represent an entire YAML document) or HikaruBase (in the case where a class represents an object that is contained within another Hikaru object).
How to Register Your Class¶
Hikaru provides the register_version_kind_class() function to enable you to associate a HikaruDocumentBase subclass with a version/kind pair. You can also optionally specify a particular release to register the class, otherwise it will be registered to the default release for the calling thread.
When registering, you must supply the apiVersion
and kind
values that will serve as the
key for determining when to use your custom class. If you’re extending an existing class,
usually the best approach is to use the apiVersion
and kind
class attributes of the
class you are subclassing. So for example, if you want to register your custom class MyPod
that is a subclass of Pod
, you can use Pod's
class attributes to supply the
apiVersion
and kind
values:
register_version_kind_class(MyPod, Pod.apiVersion, Pod.kind)
Now, when Hikaru sees this particular combination of apiVersion
and kind
, it will create
an instance of the MyPod
class instead of the Pod
class as before.
Use Case: Adding Only Methods to Hikaru Classes¶
If your use case involves only the addition of methods to existing Hikaru classes and not any new instance attributes, you’ll find this the most straightforward use case for creating custom classes.
Simply create a subclass using an appropriate
HikaruDocumentBase subclass as a base (such as Pod or Deployment),
define the methods you wish, and then register this class using the
register_version_kind_class() function. Hikaru will then
create instances of this class whenever it sees the need for an instance that matches Pod’s
apiVersion
and kind
values.
As a simple example, here’s a Pod subclass that provides a create method on the Pod class that takes the namespace as a parameter, and hides the actual create method name:
from hikaru import register_version_kind_class
from hikaru.model.rel_1_26 import *
class CRUDPod(Pod):
def create_in_namespace(self, namespace: str):
if self.metadata is None:
self.metadata = ObjectMeta()
self.metadata.namespace = namespace
return self.createNamespacedPod(self.metadata.namespace)
register_version_kind_class(CRUDPod, Pod.apiVersion, Pod.kind)
While registration of the class isn’t needed to create and use the class in your code, Hikaru will now create instances of CRUDPod whenever it needs to create a Pod, for example when querying Kubernetes or loading YAML using load_full_yaml().
Bear in mind that you can always add methods on subclasses of Hikaru objects.
Use Case: Adding Instance Attributes That Aren’t Passed In¶
If your derived class requires additional instance data attributes whose values don’t need
to be passed in when creating the new instance, then the proper approach is to implement the
__post_init__()
method. This method is established by the dataclasses
machinery to
provide a hook where additional attributes can be specified but which won’t be considered
as part of the set of fields for the dataclass.
As a simple example, suppose you wanted to add a local dict to your Pod subclass. You’d add
a __post_init__()
method like the following:
from typing import Any
from hikaru import register_version_kind_class
from hikaru.model.rel_1_26 import *
class DictPod(Pod):
def __post_init__(self, client: Any = None): # NOTE THE PARAMETERS!
super(DictPod, self).__post_init__(client=client) # NOTE CALL TO SUPER!
self.my_dict = {}
# and any other attributes you want to add
register_version_kind_class(DictPod, Pod.apiVersion, Pod.kind)
The dataclass machinery ensures that __post_init__()
is called after all work to set
up the instance is done in the generated __init__()
method.
Two important aspects to note:
Every subclass of a HikaruDocumentBase subclass is passed a client object to the
__post_init__()
method. You must ensure that the signature on your method includes this argument, or there will be a runtime failure when trying to create an instance of your object. This is only required for HikaruDocumentBase subclasses; there’s no argument passed into__post_init__()
for HikaruBase subclasses.Be sure to call
super()
passing this client object along to the parent class. Again, this is only for HikaruDocumentBase subclasses.
Use Case: Adding Instance Attributes That Are Passed In¶
Note
The next two use cases involve more direct use of Python dataclass features. If not familiar
with them, the reader is advised to consult the Python documentation on the dataclasses
module to understand the constraints involved in dataclass use.
If you want additional instance attributes and want the caller to provide these to you, you can
use the special dataclasses
field type InitVar
to designate new fields that are only
part of the initialization process and are not stored as a dataclass field. This is the proper
way to add fields that must be passed in. The use of InitVar is important because, without it,
Hikaru will think that the additional field is part of the dataclass and that field will be
rendered in generated YAML, JSON, or Python dicts, which may prove to be a problem for the
consumer of these representations.
This is a bit more involved process, as it requires your new class to be made a dataclass, and to provide suitable default values for the new fields. Hikaru will not be able to supply values for these new fields as it won’t know where to acquire the data, so you’ll want to be sure they have suitable defaults and also perhaps a means to mutate their value once the instance is created.
As an example, let’s suppose we want a Pod
subclass where we can optionally pass in several
additional bits of information: two string values and a dict with some additional info. We
can create a new dataclass that makes provision for passing in this data like so:
from hikaru.model.rel_1_22 import *
from dataclasses import dataclass, InitVar
from typing import Any, Optional, Dict
from hikaru import register_version_kind_class
@dataclass
class PodPlus(Pod):
field1: InitVar[str] = 'wibble' # defaults to 'wibble' if not provided
field2: InitVar[Optional[Any]] = None
my_dict: InitVar[Optional[Dict[str, str]]] = None
def __post_init__(self, client: Any = None, field1=None, field2=None,
my_dict: InitVar[Dict[str, str]] = None):
super(PodPlus, self).__post_init__(client=client)
self.field1 = field1
self.field2 = field2
self.my_dict = my_dict if my_dict is not None else {}
register_version_kind_class(PodPlus, Pod.apiVersion, Pod.kind)
Note that every field supplied either has a default or is optional with a default; this is because the parent class already has a defaulted field and dataclasses that are subclasses can not have fields that don’t have defaults follow fields that do.
If you’re familiar with dataclasses, you might wonder why the my_dict
field doesn’t use
a field()
default specifier with a default_factory
. This is because default_factory
can’t be used with InitVar
fields. This is why we create an empty dict in the
__post_init__()
method instead of having the dataclass machinery do it for us.
Making a Class For a New Document Type¶
The main use case for completely new document (resource) types is to create a custom resource definition, or CRD. Hikaru has direct support for creating CRDs; refer to the advanced topic << SOME REF >> for details.