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:

  1. 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 your Pod 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 *

  2. 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.

  3. 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:

  1. 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.

  2. 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.