Skip to content

Commit 9eecf8f

Browse files
committed
feat: Add state_key to build per-table and per-partition state keys
1 parent 8aff4b7 commit 9eecf8f

4 files changed

Lines changed: 78 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .keys import state_key

‎cloudquery/sdk/stateclient/keys.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Builds state keys, so that plugins choose how finely they track progress without inventing key formats."""
2+
3+
from typing import Optional, Union
4+
5+
_SEPARATOR = "|"
6+
_ASSIGN = "="
7+
_ESCAPE = "\\"
8+
9+
10+
def _escape(part: str) -> str:
11+
return (
12+
part.replace(_ESCAPE, _ESCAPE * 2)
13+
.replace(_SEPARATOR, _ESCAPE + _SEPARATOR)
14+
.replace(_ASSIGN, _ESCAPE + _ASSIGN)
15+
)
16+
17+
18+
def state_key(
19+
table: str,
20+
*,
21+
client_id: Optional[str] = None,
22+
**partition: Union[str, int],
23+
) -> str:
24+
"""
25+
Returns the state key of a table's progress, optionally for one client and one partition of the table.
26+
27+
A table with one cursor uses ``state_key("items")``. A table that tracks progress per parent, account or
28+
other partition passes the fields that identify it, e.g. ``state_key("responses", form_id=form["id"])``.
29+
Each such key is its own state row: with checkpoints, a partition's state waits only for the resources of
30+
the table resolver that set it, and the CLI persists the keys changed since its last commit together.
31+
32+
Keys are readable and deterministic: partition fields are sorted, and separators in names and values are
33+
escaped, so different tables, clients or partitions never share a key.
34+
35+
Example:
36+
>>> state_key("responses", client_id="acct-1", form_id=123)
37+
'responses|client_id=acct-1|form_id=123'
38+
"""
39+
parts = [_escape(table)]
40+
if client_id is not None:
41+
parts.append(f"client_id{_ASSIGN}{_escape(client_id)}")
42+
for name in sorted(partition):
43+
parts.append(f"{_escape(name)}{_ASSIGN}{_escape(str(partition[name]))}")
44+
return _SEPARATOR.join(parts)

‎tests/stateclient/__init__.py‎

Whitespace-only changes.

‎tests/stateclient/keys.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from cloudquery.sdk.stateclient import state_key
2+
3+
4+
def test_table_key():
5+
assert state_key("items") == "items"
6+
7+
8+
def test_partition_fields_are_sorted():
9+
assert state_key("t", b="2", a=1) == state_key("t", a=1, b="2") == "t|a=1|b=2"
10+
11+
12+
def test_client_id_comes_before_partition_fields():
13+
assert state_key("t", client_id="acct", a="x") == "t|client_id=acct|a=x"
14+
15+
16+
def test_separators_are_escaped():
17+
assert state_key("t", a="x|b=y") == "t|a=x\\|b\\=y"
18+
assert state_key("t", a="x|b=y") != state_key("t", a="x", b="y")
19+
assert state_key("t", a="x\\") != state_key("t", a="x\\\\")
20+
21+
22+
def test_tables_clients_and_partitions_never_share_a_key():
23+
keys = [
24+
state_key("t"),
25+
state_key("t|a=1"),
26+
state_key("t", a=1),
27+
state_key("t", client_id="a=1"),
28+
state_key("t", client_id="c", a=1),
29+
state_key("t", client_id="c|a=1"),
30+
state_key("t", a="1|b=2"),
31+
state_key("t", a=1, b=2),
32+
]
33+
assert len(set(keys)) == len(keys)

0 commit comments

Comments
 (0)