Skip to content

Commit 3bd85bb

Browse files
authored
docs(custom-target): add documentation for custom targets (#821)
1 parent 32e0524 commit 3bd85bb

File tree

4 files changed

+192
-6
lines changed

4 files changed

+192
-6
lines changed

docs/docs/custom_ops/custom_functions.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Custom Functions
3-
description: Build Custom Functions
3+
description: Build powerful custom functions in CocoIndex for data transformation and processing. Create standalone functions or advanced function specs with executors, including caching, GPU support, and configurable behavior for scalable data operations.
44
---
55

66
import Tabs from '@theme/Tabs';
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
title: Custom Targets
3+
description: Learn how to create custom targets in CocoIndex to export data to any destination including databases, cloud storage, file systems, and APIs. Build target specs and connectors with setup and data methods for flexible data export operations.
4+
toc_max_heading_level: 4
5+
---
6+
7+
import Tabs from '@theme/Tabs';
8+
import TabItem from '@theme/TabItem';
9+
10+
A custom target allows you to export data to any destination you want, such as databases, cloud storage, file systems, APIs, or other external systems.
11+
12+
Custom targets are defined by two components:
13+
14+
* A **target spec** that configures the behavior and connection parameters for the target.
15+
* A **target connector** that handles the actual data export operations.
16+
17+
## Target Spec
18+
19+
The target spec defines the configuration parameters for your custom target. When you use this target in a flow (typically by calling [`export()`](/docs/core/flow_def#export)), you instantiate this target spec with specific parameter values.
20+
21+
<Tabs>
22+
<TabItem value="python" label="Python" default>
23+
24+
A target spec is defined as a class that inherits from `cocoindex.op.TargetSpec`.
25+
26+
```python
27+
class CustomTarget(cocoindex.op.TargetSpec):
28+
"""
29+
Documentation for the target.
30+
"""
31+
param1: str
32+
param2: int | None = None
33+
...
34+
```
35+
36+
Notes:
37+
* All fields of the spec must have a type serializable / deserializable by the `json` module.
38+
* All subclasses of `TargetSpec` can be instantiated similar to a dataclass, i.e. `ClassName(param1=value1, param2=value2, ...)`.
39+
40+
</TabItem>
41+
</Tabs>
42+
43+
## Target Connector
44+
45+
A target connector handles the actual data export operations for your custom target. It defines how data should be written to your target destination.
46+
47+
Target connectors implement two categories of methods: **setup methods** for managing target infrastructure (similar to DDL operations in databases), and **data methods** for handling specific data operations (similar to DML operations).
48+
49+
<Tabs>
50+
<TabItem value="python" label="Python" default>
51+
52+
A target connector is defined as a class decorated by `@cocoindex.op.target_connector(spec_cls=CustomTarget)`.
53+
54+
```python
55+
@cocoindex.op.target_connector(spec_cls=CustomTarget)
56+
class CustomTargetConnector:
57+
# Setup methods
58+
@staticmethod
59+
def get_persistent_key(spec: CustomTarget, target_name: str) -> PersistentKey:
60+
"""Required. Return a persistent key that uniquely identifies this target instance."""
61+
...
62+
63+
@staticmethod
64+
def apply_setup_change(
65+
key: PersistentKey, previous: CustomTarget | None, current: CustomTarget | None
66+
) -> None:
67+
"""Required. Apply setup changes to the target."""
68+
...
69+
70+
@staticmethod
71+
def describe(key: PersistentKey) -> str:
72+
"""Optional. Return a human-readable description of the target."""
73+
...
74+
75+
# Data methods
76+
@staticmethod
77+
def prepare(spec: CustomTarget) -> PreparedCustomTarget:
78+
"""Optional. Prepare for execution before applying mutations."""
79+
...
80+
81+
@staticmethod
82+
def mutate(
83+
*all_mutations: tuple[PreparedCustomTarget, dict[DataKeyType, DataValueType | None]],
84+
) -> None:
85+
"""Required. Apply data mutations to the target."""
86+
...
87+
```
88+
89+
</TabItem>
90+
</Tabs>
91+
92+
The following data types are involved in the method definitions above: `CustomTarget`, `PersistentKey`, `PreparedCustomTarget`, `DataKeyType`, `DataValueType`. They should be replaced with the actual types in your implementation. We will explain each of them below.
93+
94+
### Setup Methods
95+
Setup methods manage the target infrastructure - creating, configuring, and cleaning up target resources.
96+
97+
#### `get_persistent_key(spec, target_name) -> PersistentKey` (Required)
98+
99+
This method returns a unique identifier for the target instance. This key is used by CocoIndex to keep track of target state and drive target spec changes.
100+
101+
The key should be stable across different runs. If a previously existing key no longer exists, CocoIndex will assume the target is gone, and will drop it by calling `apply_setup_change` with `current` set to `None`.
102+
103+
The return type of this method should be serializable by the `json` module. It will be passed to other setup methods.
104+
105+
#### `apply_setup_change(key, previous, current) -> None` (Required)
106+
107+
This method is called when the target configuration changes. It receives:
108+
- `key`: The persistent key for this target
109+
- `previous`: The previous target spec (or `None` if this is a new target)
110+
- `current`: The current target spec (or `None` if the target is being removed)
111+
112+
This method should be implemented to:
113+
- Create resources when a target is first added (`previous` is `None`)
114+
- Update configuration when a target spec changes
115+
- Clean up resources when a target is removed (`current` is `None`)
116+
117+
:::note Best practice: Keep all actions idempotent
118+
119+
Ideally this method should be idempotent, i.e. when calling this with the same arguments multiple times, the effect should remain the same.
120+
For example, if the target is a directory, it should be a no-op if we try to create it (`previous` is `None`) when the directory already exists, and also a no-op if we try to delete it (`current` is `None`) when the directory does not exist.
121+
122+
:::
123+
124+
#### `describe(key) -> str` (Optional)
125+
126+
Returns a human-readable description of the target for logging and debugging purposes.
127+
128+
129+
### Data Methods
130+
131+
Data methods handle the actual data operations - inserting, updating, and deleting records in the target.
132+
133+
#### `mutate(*all_mutations) -> None` (Required)
134+
135+
This method applies data changes to the target. It receives multiple mutation batches, where each batch is a tuple containing:
136+
137+
- The target spec (`PreparedCustomTarget`, or `CustomTarget` if `prepare` is not provided).
138+
139+
- A dictionary of mutations (`dict[DataKeyType, DataValueType | None]`).
140+
Each entry represents a mutation for a single row. When the value is `None`, it represents a deletion for the row, otherwise it's an upsert.
141+
142+
It represented in the same way as [*KTable*](/docs/core/data_types#ktable), except the value can be `None`.
143+
In particular:
144+
145+
- Since both `DataKeyType` and `DataValueType` can have multiple columns, they're [*Struct*](/docs/core/data_types#struct-types).
146+
- `DataKeyType` can be represented by a frozen dataclass (i.e. `@dataclass(frozen=True)`) or a `NamedTuple`, as it needs to be immutable.
147+
- `DataValueType` can be represented by a `dataclass`, a `NamedTuple` or a `dict[str, Any]`.
148+
149+
- For simplicity, when there're a single primary key column with basic type, we allow using type of this column (e.g. `str`, `int` etc.) as the key type, and a wrapper *Struct* type can be omitted.
150+
You can still use a `@dataclass(frozen=True)` or a `NamedTuple` to represent the key for this case though, if you want to handle both cases consistently.
151+
152+
#### `prepare(spec) -> PreparedCustomTarget` (Optional)
153+
154+
Prepares for execution by performing common operations before applying mutations. The returned value will be passed as the first element of tuples in the `mutate` method instead of the original spec.
155+
156+
```python
157+
@staticmethod
158+
def prepare(spec: CustomTarget) -> PreparedCustomTarget:
159+
"""
160+
Prepare for execution. Called once before mutations.
161+
"""
162+
# Initialize connections, validate configuration, etc.
163+
return PreparedCustomTarget(...)
164+
```
165+
166+
If not provided, the original spec will be passed directly to `mutate`.
167+
168+
169+
## Examples
170+
171+
The cocoindex repository contains the following examples of custom targets:
172+
173+
* In the [custom_output_files](https://github.com/cocoindex-io/cocoindex/blob/main/examples/custom_output_files/main.py) example, `LocalFileTarget` exports data to local HTML files.

docs/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const sidebars: SidebarsConfig = {
4949
collapsed: false,
5050
items: [
5151
'custom_ops/custom_functions',
52+
'custom_ops/custom_targets',
5253
],
5354
},
5455
{

docs/src/css/custom.css

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@
6161
--theme-color-keyline: #374151;
6262
}
6363

64+
65+
.table-of-contents {
66+
code {
67+
border: none;
68+
}
69+
}
70+
6471
.markdown {
6572
line-height: 150%;
6673

@@ -92,13 +99,15 @@
9299

93100
.navbar__title {
94101
font-family: 'Questrial', sans-serif;
95-
font-size: 1.125rem; /* 18px */
102+
font-size: 1.125rem;
103+
/* 18px */
96104
color: var(--my-color-text-black);
97105
}
98106

99107
.navbar {
100108
box-shadow: none;
101-
font-size: 0.875rem; /* 14px */
109+
font-size: 0.875rem;
110+
/* 14px */
102111
border-bottom: 1px solid var(--ifm-color-emphasis-200);
103112
}
104113

@@ -145,11 +154,13 @@
145154
}
146155

147156
.theme-doc-sidebar-menu {
148-
font-size: 0.875rem; /* 14px */
157+
font-size: 0.875rem;
158+
/* 14px */
149159
}
150160

151161
.table-of-contents {
152-
font-size: 0.8125rem; /* 13px */
162+
font-size: 0.8125rem;
163+
/* 13px */
153164
}
154165

155166
.breadcrumbs {
@@ -161,6 +172,7 @@
161172
.breadcrumbs__item:first-child {
162173
display: none;
163174
}
175+
164176
.breadcrumbs__link {
165177
padding: 0;
166178
background: none;
@@ -173,7 +185,7 @@
173185
background: var(--ifm-menu-link-sublist-icon) 50% / 1rem 1rem;
174186
}
175187

176-
.navbar__item.navbar-github-link{
188+
.navbar__item.navbar-github-link {
177189
display: block;
178190
width: 120px;
179191
}

0 commit comments

Comments
 (0)