Custom Entities¶
RTV can be extended by custom user entities (classes) to provide missing functionality for user validation scenario (e.g. implementing some custom error metrics) or extend supported configuration file formats.
Implementig and registering¶
Custom entities should implement pre-defiened framework’s interfaces.
from pydantic import BaseModel
from rtv.core.base import BaseEntity
from rtv.transformer.interfaces import ITransformer
class MyAwesomeTransformer(BaseEntity, ITransformer, idf="awesome"):
class Params(BaseModel):
my_awesome_param: int
...
def transform(self, data):
...
Usage of nested Params(BaseModel)
class provides auto-validation for your
entitiy’s parameters.
Extending from Base
class is not mandatory if you do not want to reuse any
common behaviour you can omit this.
All interface classes provided by the framework start with I
followed by the
type of the entitiy. It is important to inherit from the interface class when
realizing your custom entities because interfaces trigger some under the hood
mechanisms that allow you later to use your entitiy via configuration files.
NOTE:idf
is optional for almost all entities, and most probably not needed if
users intend to use the entity in python script, however there are some
exceptions (will be mentioned below). It is just an alias to make config files
more concise.
Following table shows which types you can implement and from which classes you should inherit in order to do that:
Entity Name |
Inherit from |
---|---|
Reader |
BaseReader, IReader |
Transformer |
BaseEntity, ITransformer |
Validation |
BaseEntity, IValidation |
Validation Strategy |
BaseValidationStrategy, IValidationStrategy |
Action * |
BaseAction, IAction |
Writer |
BaseWriter, IWriter |
Config Loader ** |
IConfigLoader, idf=”<extension_suffix>” |
* - See implementing custom actions
** - See implementing custom config loaders
More details on mentioned interfaces can be found in this section.
Implementing custom actions¶
When implementing custom actions we recommend to add a short and descriptive identifier:
# ...
class MyCustomAction(BaseAction, IAction, idf="greet"):
class Params(BaseModel):
message: str
def execute(self):
print(self.message)
# ...
That would make it more convenient to use in the configuration files.
actions:
- greet:
message: "Hello World!"
To say more, custom actions implementation only makes sense for usage in configuration files.
Implementing custom config loaders¶
IConfigLoader
is a special case, this class is not inheriting from
BaseEntity
, does not support nested Params
class, and requires idf
to be
the same as file extension suffix:
class TxtConfigLoader(IConfigLoader, idf="txt"):
...
Otherwise it should crash the run.
Registering for use in config files¶
The framework will automatically handle the addition of this custom class
to the registry
, and it will become available for use in the config files.
However, the framework needs to know where to look for the custom code. So, users
need to set up an environment variable RTV_USER_CODE_PATH
:
export RTV_USER_CODE_PATH=<custom_code_directory_path>
Substitute <custom_code_directory_path
with an actual path on your file
system where you gonna store the custom code for RTV. You can structure and
and name those files as you want.
Defining custom entities in config¶
YAML Config Example:
Using custom class name:
definitions: - name: my_awesome_transformer class: MyAwesomeTransformer my_awesome_param: 42 # ...
Using
idf
(identifier/alias):definitions: - name: my_awesome_transformer class: awesome my_awesome_param: 42 # ...
NOTE: Custom actions
should not be defined, just used by alias instead.
Using custom entities in actions¶
YAML Config Example:
Custom transformer:
actions: - transform: input: data output_name: transformed_data transformers: my_awesome_transformer # ...
Custom action:
actions: - my_awesome_action: awesome_parameter: 42 # ...
Core Interfaces¶
class IReader(Interface):
@abstractmethod
def read(
self, *args, **kwargs
) -> DataCollection:
...
class IWriter(Interface):
@abstractmethod
def write(self,
data: DataCollection,
*args, **kwargs
) -> None:
...
class ITransformer(Interface):
@abstractmethod
def transform(
self, collection: DataCollection, **kwargs
) -> DataCollection:
...
class IValidation(Interface):
@abstractmethod
def execute(
self, reference: DataCollection, target: DataCollection
) -> List[Tuple[str, ValidationResultModel]]:
...
@property
@abstractmethod
def name(self) -> str:
...
class IValidationStrategy(Interface):
@abstractmethod
def validate(
self,
key: str,
reference: DataCollection,
target: DataCollection,
) -> ValidationResultModel:
...
@property
@abstractmethod
def details(self) -> StrategyDetailsModel:
...
@property
@abstractmethod
def name(self) -> str:
...
class IAction(Interface):
@abstractmethod
def execute(self) -> None:
...
class IConfigLoader(Interface):
@abstractmethod
def load(self, config_path: str) -> Dict[str, Any]:
...