diff --git a/sdk/docs/how-to/interact-with-turing-api/01-create-a-router.md b/sdk/docs/how-to/interact-with-turing-api/01-create-a-router.md index 4aded275c..46918da72 100644 --- a/sdk/docs/how-to/interact-with-turing-api/01-create-a-router.md +++ b/sdk/docs/how-to/interact-with-turing-api/01-create-a-router.md @@ -6,7 +6,7 @@ such as the use of experiment engines, enrichers (pre-processors) or ensemblers Hence to build a router using Turing SDK, you would need to incrementally define these components and build a `RouterConfig` object (you can find more information on how these individual components need to be built in the -XXX section). +[Using Turing SDK Classes](../use-turing-sdk-classes) section). Using the example shown in the `README` of the Turing SDK documentation main page, you need to construct a `RouterConfig` instance by specifying the various components as arguments: diff --git a/sdk/docs/how-to/use-turing-sdk-classes/01-router.md b/sdk/docs/how-to/use-turing-sdk-classes/01-router.md new file mode 100644 index 000000000..d9e6667b4 --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/01-router.md @@ -0,0 +1,12 @@ +# Router + +A `Router` object represents a router that is created on Turing API. It does not (and should not) ever be created +manually by using its constructor directly. Instead, you should only be manipulating with `Router` instances that +get returned as a result of using the various `Router` class and instance methods that interact with Turing API. + +A `Router` object has attributes such as `id`, `name`, `project_id`, `environment_name`, `monitoring_url`, `status` +and `endpoint`. It also has a `config` attribute, which is a `RouterConfig` containing the current configuration for +the router. + +When trying to replicate configuration from an existing router, always retrieve the underlying `RouterConfig` from +the `Router` instance by accessing its `config` attribute. \ No newline at end of file diff --git a/sdk/docs/how-to/use-turing-sdk-classes/02-router-version.md b/sdk/docs/how-to/use-turing-sdk-classes/02-router-version.md new file mode 100644 index 000000000..6528f664b --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/02-router-version.md @@ -0,0 +1,13 @@ +# RouterVersion + +A `RouterVersion` represents a single version (and configuration) of a Turing Router. Just as `Router` objects, they +should almost never be created manually by using their constructor. + +Besides assessing attributes of a `RouterVersion` object directly, which will allow you to access attributes such as +`id`, `version`, `created_at`, `updated_at`, `environment_name`, `status`, `name`, `monitoring_url`, `log_config`, +you may also consider retrieving the entire router configuration from a specific `RouterVersion` object as a +`RouterConfig` for further manipulation: + +```python +my_config = router_version.get_config() +``` diff --git a/sdk/docs/how-to/use-turing-sdk-classes/03-router-config.md b/sdk/docs/how-to/use-turing-sdk-classes/03-router-config.md new file mode 100644 index 000000000..a0fc3bdef --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/03-router-config.md @@ -0,0 +1,46 @@ +# RouterConfig + +`RouterConfig` objects are what you would probably interact most frequently with when using Turing SDK. They +essentially carry a router's configuration and define the ways in which a router should be run. All of the +methods that interact with Turing API that involve the updating or creating of routers involve the +use of `RouterConfig` objects as arguments. + +As you would have seen before, a `RouterConfig` object is built using multiple parts: + +```python +@dataclass +class RouterConfig: + """ + Class to create a new RouterConfig. Can be built up from its individual components or initialised instantly + from an appropriate API response + + :param environment_name: name of the environment + :param name: name of the router + :param routes: list of routes used by the router + :param rules: list of rules used by the router + :param default_route_id: default route id to be used + :param experiment_engine: experiment engine config file + :param resource_request: resources to be provisioned for the router + :param timeout: request timeout which when exceeded, the request to the router will be terminated + :param log_config: logging config settings to be used with the router + :param enricher: enricher config settings to be used with the router + :param ensembler: ensembler config settings to be used with the router + """ + environment_name: str + name: str + routes: Union[List[Route], List[Dict[str, str]]] = None + rules: Union[List[TrafficRule], List[Dict]] = None + default_route_id: str = None + experiment_engine: Union[ExperimentConfig, Dict] = None + resource_request: Union[ResourceRequest, Dict[str, Union[str, int]]] = None + timeout: str = None + log_config: Union[LogConfig, Dict[str, Union[str, bool, int]]] = None + enricher: Union[Enricher, Dict] = None + ensembler: Union[RouterEnsemblerConfig, Dict] = None +``` + +When constructing a `RouterConfig` object from scratch, it is **highly recommended** that you construct each +individual component using the Turing SDK classes provided instead of using `dict` objects which do not perform any +schema validation. + +In the following pages of this subsection, we will go through the usage of these individual components separately. diff --git a/sdk/docs/how-to/use-turing-sdk-classes/04-route.md b/sdk/docs/how-to/use-turing-sdk-classes/04-route.md new file mode 100644 index 000000000..01ea00eb4 --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/04-route.md @@ -0,0 +1,19 @@ +# Route + +Creating a route with Turing SDK is just as simple as doing it on the UI; one only needs to specify the `id`, +`endpoint`, and `timeout` of the route: + +```python +@dataclass +class Route: + """ + Class to create a new Route object + + :param id: route's name + :param endpoint: endpoint of the route. Must be a valid URL + :param timeout: timeout indicating the duration past which the request execution will end + """ + id: str + endpoint: str + timeout: str +``` diff --git a/sdk/docs/how-to/use-turing-sdk-classes/05-experiment-config.md b/sdk/docs/how-to/use-turing-sdk-classes/05-experiment-config.md new file mode 100644 index 000000000..0974ebd03 --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/05-experiment-config.md @@ -0,0 +1,17 @@ +# ExperimentConfig + +The `ExperimentConfig` class is a simple container to carry configuration related to an experiment to be used by a +Turing Router. Note that as Turing does not create experiments automatically, you would need to create your +experiments separately prior to specifying their configuration here. + +Also, notice that `ExperimentConfig` does not contain any fixed schema as it simply carries configuration for +generic experiment engines, which are used as plug-ins for Turing. When building an `ExperimentConfig` from scratch, +you would need to consider the underlying schema for the `config` attribute as well as the appropriate `type` that +corresponds to your selected experiment engine: + +```python +@dataclass +class ExperimentConfig: + type: str = "nop" + config: Dict = None +``` \ No newline at end of file diff --git a/sdk/docs/how-to/use-turing-sdk-classes/06-resource-request.md b/sdk/docs/how-to/use-turing-sdk-classes/06-resource-request.md new file mode 100644 index 000000000..bf84faaba --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/06-resource-request.md @@ -0,0 +1,19 @@ +# ResourceRequest + +A `ResourceRequest` class carries information related to the resources that should be allocated to a particular +component, e.g. router, ensembler, enricher, etc., and is defined by 4 attributes, `min_replica`, `max_replica`, +`cpu_request`, `memory_request`: + +```python +@dataclass +class ResourceRequest: + min_allowed_replica: ClassVar[int] = 0 + max_allowed_replica: ClassVar[int] = 20 + min_replica: int + max_replica: int + cpu_request: str + memory_request: str +``` + +Note that the units for CPU and memory requests are measured in cpu units and bytes respectively. You may wish to +read more about how these are measured [here](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/). \ No newline at end of file diff --git a/sdk/docs/how-to/use-turing-sdk-classes/07-log-config.md b/sdk/docs/how-to/use-turing-sdk-classes/07-log-config.md new file mode 100644 index 000000000..2b797c796 --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/07-log-config.md @@ -0,0 +1,65 @@ +# LogConfig + +Logging for Turing Routers is done through BigQuery or Kafka, and its configuration is managed by the `LogConfig` +class. Two helper classes (child classes of `LogConfig`) have been created to assist you in constructing these objects: + +```python +@dataclass +class BigQueryLogConfig(LogConfig): + """ + Class to create a new log config with a BigQuery config + + :param table: name of the BigQuery table; if the table does not exist, it will be created automatically + :param service_account_secret: service account which has both JobUser and DataEditor privileges and write access + :param batch_load: optional parameter to indicate if batch loading is used + """ + def __init__(self, + table: str, + service_account_secret: str, + batch_load: bool = None): + self.table = table + self.service_account_secret = service_account_secret + self.batch_load = batch_load + + super().__init__(result_logger_type=ResultLoggerType.BIGQUERY) +``` + +```python +@dataclass +class KafkaLogConfig(LogConfig): + def __init__(self, + brokers: str, + topic: str, + serialization_format: KafkaConfigSerializationFormat): + """ + Method to create a new log config with a Kafka config + + :param brokers: comma-separated list of one or more Kafka brokers + :param topic: valid Kafka topic name on the server; data will be written to this topic + :param serialization_format: message serialization format to be used + """ + self.brokers = brokers + self.topic = topic + self.serialization_format = serialization_format + + super().__init__(result_logger_type=ResultLoggerType.KAFKA) +``` + +If you are using a `KafkaLogConfig`, you would additionally have to define a `serialization_format`, which is of a +`KafkaConfigSerializationFormat`: + +```python +class KafkaConfigSerializationFormat(Enum): + JSON = "json" + PROTOBUF = "protobuf" +``` + +If you do not intend to use any logging, simply create a regular `LogConfig` object with `result_loggger_type` set +as `ResultLoggerType.NOP`, without defining the other arguments: + +```python +log_config = LogConfig(result_logger_type=ResultLoggerType.NOP) +``` + +While `ResultLoggerType` may take on the `enum` value of `ResultLoggerType.CONSOLE`, its behaviour is +currently undefined and you will almost certainly experience errors while using it. \ No newline at end of file diff --git a/sdk/docs/how-to/use-turing-sdk-classes/08-enricher.md b/sdk/docs/how-to/use-turing-sdk-classes/08-enricher.md new file mode 100644 index 000000000..ffae2c277 --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/08-enricher.md @@ -0,0 +1,40 @@ +# Enricher + +An `Enricher` object holds configuration needed to define an enricher: + +```python +@dataclass +class Enricher: + """ + Class to create a new Enricher + + :param image: registry and name of the image + :param resource_request: ResourceRequest instance containing configs related to the resources required + :param endpoint: endpoint URL of the enricher + :param timeout: request timeout which when exceeded, the request to the enricher will be terminated + :param port: port number exposed by the container + :param env: environment variables required by the container + :param id: id of the enricher + :param service_account: optional service account for the Docker deployment + """ + image: str + resource_request: ResourceRequest + endpoint: str + timeout: str + port: int + env: List['EnvVar'] + id: int = None + service_account: str = None +``` + +## EnvVar + +To define environment variables for the `env` attribute in an `Enricher`, you would need to define them using the +`EnvVar` object: + +```python +@dataclass +class EnvVar: + name: str + value: str +``` \ No newline at end of file diff --git a/sdk/docs/how-to/use-turing-sdk-classes/09-router-ensembler-config.md b/sdk/docs/how-to/use-turing-sdk-classes/09-router-ensembler-config.md new file mode 100644 index 000000000..5faad115f --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/09-router-ensembler-config.md @@ -0,0 +1,74 @@ +# RouterEnsemblerConfig + +Ensembling for Turing Routers is done through Standard, Docker or Pyfunc ensemblers, and its configuration is +managed by the `RouterEnsemblerConfig` class. Three helper classes (child classes of `RouterEnsemblerConfig`) have been +created to assist you in constructing these objects: + +```python +@dataclass +class StandardRouterEnsemblerConfig(RouterEnsemblerConfig): + def __init__(self, + experiment_mappings: List[Dict[str, str]]): + """ + Method to create a new standard ensembler + + :param experiment_mappings: configured mappings between routes and treatments + """ + self.experiment_mappings = experiment_mappings + super().__init__(type="standard") +``` + +```python +@dataclass +class DockerRouterEnsemblerConfig(RouterEnsemblerConfig): + def __init__(self, + image: str, + resource_request: ResourceRequest, + endpoint: str, + timeout: str, + port: int, + env: List['EnvVar'], + service_account: str = None): + """ + Method to create a new Docker ensembler + + :param image: registry and name of the image + :param resource_request: ResourceRequest instance containing configs related to the resources required + :param endpoint: endpoint URL of the ensembler + :param timeout: request timeout which when exceeded, the request to the ensembler will be terminated + :param port: port number exposed by the container + :param env: environment variables required by the container + :param service_account: optional service account for the Docker deployment + """ + self.image = image + self.resource_request = resource_request + self.endpoint = endpoint + self.timeout = timeout + self.port = port + self.env = env + self.service_account = service_account + super().__init__(type="docker") +``` + +```python +@dataclass +class PyfuncRouterEnsemblerConfig(RouterEnsemblerConfig): + def __init__(self, + project_id: int, + ensembler_id: int, + timeout: str, + resource_request: ResourceRequest): + """ + Method to create a new Pyfunc ensembler + + :param project_id: project id of the current project + :param ensembler_id: ensembler_id of the ensembler + :param resource_request: ResourceRequest instance containing configs related to the resources required + :param timeout: request timeout which when exceeded, the request to the ensembler will be terminated + """ + self.project_id = project_id + self.ensembler_id = ensembler_id + self.resource_request = resource_request + self.timeout = timeout + super().__init__(type="pyfunc") +``` diff --git a/sdk/docs/how-to/use-turing-sdk-classes/10-traffic-rule.md b/sdk/docs/how-to/use-turing-sdk-classes/10-traffic-rule.md new file mode 100644 index 000000000..35ab28108 --- /dev/null +++ b/sdk/docs/how-to/use-turing-sdk-classes/10-traffic-rule.md @@ -0,0 +1,53 @@ +# TrafficRule + +Each traffic rule is defined by at least one `TrafficRuleCondition` and one route. Routes are essentially the `id`s +of `Route` objects that you intend to specify for the entire `TrafficRule`. + +```python +@dataclass +class TrafficRule: + """ + Class to create a new TrafficRule based on a list of conditions and routes + + :param conditions: list of TrafficRuleConditions that need to ALL be satisfied before routing to the given routes + :param routes: list of routes to send the request to should all the given conditions be met + """ + conditions: Union[List[TrafficRuleCondition], List[Dict[str, List[str]]]] + routes: List[str] +``` + +## Traffic Rule Condition + +When defining a traffic rule, one would need to decide between using a `HeaderTrafficRuleCondition` or a +`PayloadTrafficRuleCondition`. These subclasses can be used to build a `TrafficRuleCondition` without having to +manually set attributes such as `field_source` or `operator`: + +```python +@dataclass +class HeaderTrafficRuleCondition(TrafficRuleCondition): + def __init__(self, + field: str, + values: List[str]): + """ + Method to create a new TrafficRuleCondition that is defined on a request header + + :param field: name of the field specified + :param values: values that are supposed to match those found in the field + """ + super().__init__(field_source=FieldSource.HEADER, field=field, operator="in", values=values) + + +@dataclass +class PayloadTrafficRuleCondition(TrafficRuleCondition): + def __init__(self, + field: str, + values: List[str]): + """ + Method to create a new TrafficRuleCondition that is defined on a request payload + + :param field: name of the field specified + :param values: values that are supposed to match those found in the field + """ + super().__init__(field_source=FieldSource.PAYLOAD, field=field, operator="in", values=values) +``` + diff --git a/sdk/tests/router/config/traffic_rule_test.py b/sdk/tests/router/config/traffic_rule_test.py index 7071778ab..e6da23e7e 100644 --- a/sdk/tests/router/config/traffic_rule_test.py +++ b/sdk/tests/router/config/traffic_rule_test.py @@ -107,7 +107,7 @@ def test_create_payload_traffic_rule_condition(field, values, expected, request) [ HeaderTrafficRuleCondition( field="x-region", - values= ["region-a", "region-b"], + values=["region-a", "region-b"], ), PayloadTrafficRuleCondition( field="service_type.id",