cloudquery-plugin-sdk 0.1.55__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. cloudquery/sdk/__init__.py +0 -0
  2. cloudquery/sdk/internal/__init__.py +0 -0
  3. cloudquery/sdk/internal/checkpoint.py +199 -0
  4. cloudquery/sdk/internal/memdb/__init__.py +1 -0
  5. cloudquery/sdk/internal/memdb/memdb.py +221 -0
  6. cloudquery/sdk/internal/servers/__init__.py +0 -0
  7. cloudquery/sdk/internal/servers/discovery_v1/__init__.py +0 -0
  8. cloudquery/sdk/internal/servers/discovery_v1/discovery.py +11 -0
  9. cloudquery/sdk/internal/servers/plugin_v3/__init__.py +1 -0
  10. cloudquery/sdk/internal/servers/plugin_v3/plugin.py +156 -0
  11. cloudquery/sdk/message/__init__.py +14 -0
  12. cloudquery/sdk/message/read.py +6 -0
  13. cloudquery/sdk/message/sync.py +31 -0
  14. cloudquery/sdk/message/write.py +24 -0
  15. cloudquery/sdk/plugin/__init__.py +1 -0
  16. cloudquery/sdk/plugin/plugin.py +102 -0
  17. cloudquery/sdk/py.typed +0 -0
  18. cloudquery/sdk/scalar/__init__.py +15 -0
  19. cloudquery/sdk/scalar/binary.py +35 -0
  20. cloudquery/sdk/scalar/bool.py +48 -0
  21. cloudquery/sdk/scalar/date32.py +39 -0
  22. cloudquery/sdk/scalar/date64.py +39 -0
  23. cloudquery/sdk/scalar/float.py +49 -0
  24. cloudquery/sdk/scalar/int.py +56 -0
  25. cloudquery/sdk/scalar/json.py +27 -0
  26. cloudquery/sdk/scalar/list.py +56 -0
  27. cloudquery/sdk/scalar/scalar.py +30 -0
  28. cloudquery/sdk/scalar/scalar_factory.py +64 -0
  29. cloudquery/sdk/scalar/string.py +31 -0
  30. cloudquery/sdk/scalar/timestamp.py +41 -0
  31. cloudquery/sdk/scalar/uint.py +58 -0
  32. cloudquery/sdk/scalar/uuid.py +43 -0
  33. cloudquery/sdk/scalar/vector.py +43 -0
  34. cloudquery/sdk/scheduler/__init__.py +3 -0
  35. cloudquery/sdk/scheduler/scheduler.py +267 -0
  36. cloudquery/sdk/scheduler/table_resolver.py +48 -0
  37. cloudquery/sdk/schema/__init__.py +16 -0
  38. cloudquery/sdk/schema/arrow.py +11 -0
  39. cloudquery/sdk/schema/column.py +77 -0
  40. cloudquery/sdk/schema/resource.py +29 -0
  41. cloudquery/sdk/schema/table.py +332 -0
  42. cloudquery/sdk/serve/__init__.py +1 -0
  43. cloudquery/sdk/serve/plugin.py +407 -0
  44. cloudquery/sdk/stateclient/__init__.py +1 -0
  45. cloudquery/sdk/stateclient/keys.py +44 -0
  46. cloudquery/sdk/stateclient/stateclient.py +278 -0
  47. cloudquery/sdk/transformers/__init__.py +2 -0
  48. cloudquery/sdk/transformers/openapi.py +49 -0
  49. cloudquery/sdk/transformers/transformers.py +13 -0
  50. cloudquery/sdk/types/__init__.py +2 -0
  51. cloudquery/sdk/types/json.py +26 -0
  52. cloudquery/sdk/types/uuid.py +28 -0
  53. cloudquery_plugin_sdk-0.1.55.dist-info/METADATA +50 -0
  54. cloudquery_plugin_sdk-0.1.55.dist-info/RECORD +57 -0
  55. cloudquery_plugin_sdk-0.1.55.dist-info/WHEEL +5 -0
  56. cloudquery_plugin_sdk-0.1.55.dist-info/licenses/LICENSE +353 -0
  57. cloudquery_plugin_sdk-0.1.55.dist-info/top_level.txt +1 -0
File without changes
File without changes
@@ -0,0 +1,199 @@
1
+ """
2
+ Orders state checkpoints after the resources they summarize.
3
+
4
+ When the CLI supports checkpoints, it persists the state rows a source sends as
5
+ ``Sync.MessageCheckpoint`` once every destination has committed the messages sent
6
+ before them. A checkpoint must therefore go on the sync stream only after every
7
+ resource its state covers: the resources produced, before the flush, by the table
8
+ resolvers that changed its keys, and the relations those resources spawned.
9
+
10
+ The scheduler sends every message through a single queue, so a checkpoint put on
11
+ the queue once those resources are on it is ordered after them on the stream.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import contextvars
17
+ import queue
18
+ import threading
19
+ from contextlib import contextmanager
20
+ from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple
21
+
22
+ from cloudquery.sdk.message import SyncCheckpointMessage
23
+
24
+
25
+ class Scope:
26
+ """
27
+ A table resolver call and the relations its resources spawn.
28
+
29
+ A scope is finished once its resolver returned and every child scope it
30
+ spawned is finished.
31
+ """
32
+
33
+ def __init__(self, parent: Optional[Scope], sync: Optional[object]):
34
+ self.parent = parent
35
+ # the sync the scope belongs to, so that resolvers still running after it ended cannot checkpoint into the next one
36
+ self.sync = sync
37
+ # child scopes that are not finished yet
38
+ self.open_children: Set[Scope] = set()
39
+ self.resolving = True
40
+ # checkpoints waiting for this scope to finish
41
+ self.waiters: List[_Pending] = []
42
+
43
+ def finished(self) -> bool:
44
+ return not self.resolving and not self.open_children
45
+
46
+
47
+ class _Pending:
48
+ """A checkpoint waiting for scopes to finish and for earlier checkpoints of its keys to be sent."""
49
+
50
+ def __init__(self, msg: SyncCheckpointMessage, keys: List[str], sync: object):
51
+ self.msg = msg
52
+ self.keys = keys
53
+ self.sync = sync
54
+ self.blockers = 0
55
+ self.dependents: List[_Pending] = []
56
+
57
+
58
+ class Checkpoints:
59
+ """
60
+ Sends state checkpoints on the scheduler's queue after the resources they cover.
61
+
62
+ The scheduler owns an instance and passes it to the state client through
63
+ :meth:`cloudquery.sdk.stateclient.StateClientBuilder.build`.
64
+ """
65
+
66
+ def __init__(self):
67
+ self._lock = threading.Lock()
68
+ self._scope: contextvars.ContextVar[Optional[Scope]] = contextvars.ContextVar(
69
+ "cloudquery_checkpoint_scope", default=None
70
+ )
71
+ self._res: Optional[queue.Queue] = None
72
+ # newest unsent checkpoint of each key
73
+ self._unsent: Dict[str, _Pending] = {}
74
+ self._sync: Optional[object] = None
75
+
76
+ def start(self, res: queue.Queue) -> None:
77
+ """Sends later checkpoints on res. Called by the scheduler when a sync starts."""
78
+ with self._lock:
79
+ self._res = res
80
+ self._sync = object()
81
+ self._unsent = {}
82
+
83
+ def stop(self) -> None:
84
+ """Drops unsent checkpoints and rejects later ones. Called by the scheduler when a sync ends."""
85
+ with self._lock:
86
+ self._res = None
87
+ self._sync = None
88
+ self._unsent = {}
89
+
90
+ def new_scope(self, parent: Optional[Scope]) -> Scope:
91
+ """Returns a scope for a table resolver call, before the call is submitted."""
92
+ scope = Scope(parent, parent.sync if parent is not None else self._sync)
93
+ if parent is not None:
94
+ with self._lock:
95
+ parent.open_children.add(scope)
96
+ return scope
97
+
98
+ @contextmanager
99
+ def entered(self, scope: Scope) -> Iterator[None]:
100
+ """
101
+ Makes scope the current one in this context, for the state changes its resolver makes.
102
+ To change state from other threads, run them in a copy of the context, e.g. with
103
+ ``contextvars.copy_context().run``; asyncio tasks copy it already.
104
+ """
105
+ token = self._scope.set(scope)
106
+ try:
107
+ yield
108
+ finally:
109
+ self._scope.reset(token)
110
+
111
+ def current_scope(self) -> Optional[Scope]:
112
+ """Returns the scope of the table resolver running in this context, or None outside table resolvers."""
113
+ return self._scope.get()
114
+
115
+ def resolver_finished(self, scope: Scope) -> None:
116
+ """
117
+ Marks the resolver of scope as returned, once every resource it produced is on the queue.
118
+ """
119
+ with self._lock:
120
+ scope.resolving = False
121
+ ready: List[_Pending] = []
122
+ while scope is not None and scope.finished():
123
+ for pending in scope.waiters:
124
+ pending.blockers -= 1
125
+ if pending.blockers == 0:
126
+ ready.append(pending)
127
+ scope.waiters = []
128
+ if scope.parent is not None:
129
+ scope.parent.open_children.discard(scope)
130
+ scope = scope.parent
131
+ self._send(ready)
132
+
133
+ def schedule(
134
+ self,
135
+ checkpoints: List[
136
+ Tuple[Iterable[Optional[Scope]], List[str], SyncCheckpointMessage]
137
+ ],
138
+ ) -> None:
139
+ """
140
+ Sends each (scopes, keys, msg) once every resource the scopes produced so far is on the
141
+ queue, including the relations spawned from them, and after every earlier unsent
142
+ checkpoint of keys. A None scope stands for changes made outside table resolvers, which
143
+ wait for nothing. Schedules all of them or, if it raises, none of them.
144
+
145
+ Raises:
146
+ RuntimeError: if no sync is running, as the checkpoints could not be sent.
147
+ """
148
+ with self._lock:
149
+ if self._res is None:
150
+ raise RuntimeError(
151
+ "state checkpoints can only be flushed while the scheduler syncs, "
152
+ "e.g. from a table resolver or the post-sync hook"
153
+ )
154
+ for scopes, _, _ in checkpoints:
155
+ if any(s is not None and s.sync is not self._sync for s in scopes):
156
+ raise RuntimeError(
157
+ "state checkpoints of a table resolver can only be flushed during its own sync"
158
+ )
159
+ for scopes, keys, msg in checkpoints:
160
+ self._schedule(scopes, keys, msg)
161
+
162
+ def _schedule(
163
+ self,
164
+ scopes: Iterable[Optional[Scope]],
165
+ keys: List[str],
166
+ msg: SyncCheckpointMessage,
167
+ ) -> None:
168
+ pending = _Pending(msg, keys, self._sync)
169
+ for scope in {s for s in scopes if s is not None}:
170
+ # the scope's own resources are on the queue already: resolvers put each
171
+ # resource on it before they resume, so only its relations can be in flight
172
+ for child in scope.open_children:
173
+ child.waiters.append(pending)
174
+ pending.blockers += 1
175
+ earlier = set()
176
+ for key in keys:
177
+ previous = self._unsent.get(key)
178
+ if previous is not None and previous not in earlier:
179
+ earlier.add(previous)
180
+ previous.dependents.append(pending)
181
+ pending.blockers += 1
182
+ self._unsent[key] = pending
183
+ if pending.blockers == 0:
184
+ self._send([pending])
185
+
186
+ def _send(self, ready: List[_Pending]) -> None:
187
+ """Puts ready checkpoints on the queue, then the ones that only waited for them. Called with the lock held."""
188
+ while ready:
189
+ pending = ready.pop()
190
+ if pending.sync is self._sync:
191
+ self._res.put(pending.msg)
192
+ for key in pending.keys:
193
+ if self._unsent.get(key) is pending:
194
+ del self._unsent[key]
195
+ for dependent in pending.dependents:
196
+ dependent.blockers -= 1
197
+ if dependent.blockers == 0:
198
+ ready.append(dependent)
199
+ pending.dependents = []
@@ -0,0 +1 @@
1
+ from .memdb import MemDB
@@ -0,0 +1,221 @@
1
+ import json
2
+
3
+ from cloudquery.sdk import plugin
4
+ from cloudquery.sdk import message
5
+ from cloudquery.sdk import schema
6
+ from cloudquery.sdk.scheduler import Scheduler, TableResolver
7
+ from typing import List, Generator, Dict, Any
8
+ import pyarrow as pa
9
+ from cloudquery.sdk.schema.table import Table
10
+ from cloudquery.sdk.schema.arrow import METADATA_TABLE_NAME
11
+ from cloudquery.sdk.types import JSONType
12
+ from dataclasses import dataclass, field
13
+
14
+ NAME = "memdb"
15
+ VERSION = "development"
16
+
17
+
18
+ class Client:
19
+ def __init__(self) -> None:
20
+ pass
21
+
22
+ def id(self):
23
+ return "memdb"
24
+
25
+
26
+ class MemDBResolver(TableResolver):
27
+ def __init__(
28
+ self, table: Table, records: List, child_resolvers: list[TableResolver] = None
29
+ ) -> None:
30
+ super().__init__(table=table, child_resolvers=child_resolvers)
31
+ self._records = records
32
+
33
+ def resolve(self, client: None, parent_resource) -> Generator[Any, None, None]:
34
+ for record in self._records:
35
+ yield record
36
+
37
+
38
+ class Table1Relation1(Table):
39
+ def __init__(self) -> None:
40
+ super().__init__(
41
+ name="table_1_relation_1",
42
+ columns=[
43
+ schema.Column(
44
+ name="name",
45
+ type=pa.string(),
46
+ primary_key=True,
47
+ not_null=True,
48
+ unique=True,
49
+ ),
50
+ schema.Column(name="data", type=JSONType()),
51
+ ],
52
+ title="Table 1 Relation 1",
53
+ description="Test Table 1 Relation 1",
54
+ )
55
+
56
+ @property
57
+ def resolver(self):
58
+ return MemDBResolver(
59
+ self,
60
+ records=[
61
+ {"name": "a", "data": {"a": 1}},
62
+ {"name": "b", "data": {"b": 2}},
63
+ {"name": "c", "data": {"c": 3}},
64
+ ],
65
+ )
66
+
67
+
68
+ class Table1(Table):
69
+ def __init__(self) -> None:
70
+ super().__init__(
71
+ name="table_1",
72
+ columns=[
73
+ schema.Column(
74
+ name="name",
75
+ type=pa.string(),
76
+ primary_key=True,
77
+ not_null=True,
78
+ unique=True,
79
+ ),
80
+ schema.Column(
81
+ name="id",
82
+ type=pa.int64(),
83
+ primary_key=True,
84
+ not_null=True,
85
+ unique=True,
86
+ incremental_key=True,
87
+ ),
88
+ ],
89
+ title="Table 1",
90
+ description="Test Table 1",
91
+ is_incremental=True,
92
+ relations=[Table1Relation1()],
93
+ )
94
+
95
+ @property
96
+ def resolver(self):
97
+ child_resolvers: list[TableResolver] = []
98
+ for rel in self.relations:
99
+ child_resolvers.append(rel.resolver)
100
+
101
+ return MemDBResolver(
102
+ self,
103
+ records=[
104
+ {"name": "a", "id": 1},
105
+ {"name": "b", "id": 2},
106
+ {"name": "c", "id": 3},
107
+ ],
108
+ child_resolvers=child_resolvers,
109
+ )
110
+
111
+
112
+ class Table2(Table):
113
+ def __init__(self) -> None:
114
+ super().__init__(
115
+ name="table_2",
116
+ columns=[
117
+ schema.Column(
118
+ name="name",
119
+ type=pa.string(),
120
+ primary_key=True,
121
+ not_null=True,
122
+ unique=True,
123
+ ),
124
+ schema.Column(name="id", type=pa.int64()),
125
+ ],
126
+ title="Table 2",
127
+ description="Test Table 2",
128
+ )
129
+
130
+ @property
131
+ def resolver(self):
132
+ return MemDBResolver(
133
+ self,
134
+ records=[
135
+ {"name": "a", "id": 1},
136
+ {"name": "b", "id": 2},
137
+ {"name": "c", "id": 3},
138
+ ],
139
+ )
140
+
141
+
142
+ @dataclass
143
+ class Spec:
144
+ concurrency: int = field(default=1000)
145
+ queue_size: int = field(default=1000)
146
+
147
+
148
+ class MemDB(plugin.Plugin):
149
+ def __init__(self) -> None:
150
+ super().__init__(
151
+ NAME, VERSION, opts=plugin.plugin.Options(team="cloudquery", kind="source")
152
+ )
153
+ table1 = Table1()
154
+ table2 = Table2()
155
+ self._tables: Dict[str, schema.Table] = {
156
+ table1.name: table1,
157
+ table2.name: table2,
158
+ }
159
+ self._db: List[pa.RecordBatch] = []
160
+ self._client = Client()
161
+
162
+ def set_logger(self, logger) -> None:
163
+ self._logger = logger
164
+
165
+ def init(self, spec, no_connection: bool = False):
166
+ if no_connection:
167
+ return
168
+ self._spec_json = json.loads(spec)
169
+ self._spec = Spec(**self._spec_json)
170
+ self._scheduler = Scheduler(
171
+ concurrency=self._spec.concurrency,
172
+ queue_size=self._spec.queue_size,
173
+ logger=self._logger,
174
+ )
175
+
176
+ def get_tables(self, options: plugin.TableOptions = None) -> List[plugin.Table]:
177
+ tables = list(self._tables.values())
178
+
179
+ # set parent table relationships
180
+ for table in tables:
181
+ for relation in table.relations:
182
+ relation.parent = table
183
+
184
+ return schema.filter_dfs(tables, options.tables, options.skip_tables)
185
+
186
+ def sync(
187
+ self, options: plugin.SyncOptions
188
+ ) -> Generator[message.SyncMessage, None, None]:
189
+ resolvers: list[TableResolver] = []
190
+ for table in self.get_tables(
191
+ plugin.TableOptions(
192
+ tables=options.tables,
193
+ skip_tables=options.skip_tables,
194
+ skip_dependent_tables=options.skip_dependent_tables,
195
+ )
196
+ ):
197
+ resolvers.append(table.resolver)
198
+
199
+ return self._scheduler.sync(
200
+ self._client, resolvers, options.deterministic_cq_id
201
+ )
202
+
203
+ def write(self, writer: Generator[message.WriteMessage, None, None]) -> None:
204
+ for msg in writer:
205
+ if isinstance(msg, message.WriteMigrateTableMessage):
206
+ pass
207
+ elif isinstance(msg, message.WriteInsertMessage):
208
+ self._db.append(msg.record)
209
+ else:
210
+ raise NotImplementedError(f"Unknown message type {type(msg)}")
211
+
212
+ def read(self, table: Table) -> Generator[message.ReadMessage, None, None]:
213
+ for record in self._db:
214
+ recordMetadata = record.schema.metadata.get(METADATA_TABLE_NAME).decode(
215
+ "utf-8"
216
+ )
217
+ if recordMetadata == table.name:
218
+ yield message.ReadMessage(record)
219
+
220
+ def close(self) -> None:
221
+ self._db = {}
File without changes
@@ -0,0 +1,11 @@
1
+ from typing import List
2
+
3
+ from cloudquery.discovery_v1 import discovery_pb2, discovery_pb2_grpc
4
+
5
+
6
+ class DiscoveryServicer(discovery_pb2_grpc.DiscoveryServicer):
7
+ def __init__(self, versions: List[int]):
8
+ self._versions = versions
9
+
10
+ def GetVersions(self, request, context):
11
+ return discovery_pb2.GetVersions.Response(versions=self._versions)
@@ -0,0 +1 @@
1
+ from .plugin import PluginServicer
@@ -0,0 +1,156 @@
1
+ from typing import Generator
2
+
3
+ import pyarrow as pa
4
+ import structlog
5
+ from cloudquery.plugin_v3 import plugin_pb2, plugin_pb2_grpc, arrow
6
+
7
+ from cloudquery.sdk.message import (
8
+ SyncInsertMessage,
9
+ SyncMigrateTableMessage,
10
+ SyncErrorMessage,
11
+ SyncCheckpointMessage,
12
+ WriteInsertMessage,
13
+ WriteMigrateTableMessage,
14
+ WriteMessage,
15
+ WriteDeleteStale,
16
+ )
17
+ from cloudquery.sdk.plugin.plugin import (
18
+ BackendOptions,
19
+ Plugin,
20
+ SyncOptions,
21
+ TableOptions,
22
+ )
23
+ from cloudquery.sdk.schema import tables_to_arrow_schemas, Table
24
+ from cloudquery.sdk.schema.table import flatten_tables
25
+
26
+
27
+ class PluginServicer(plugin_pb2_grpc.PluginServicer):
28
+ def __init__(self, plugin: Plugin, logger=None):
29
+ self._logger = logger if logger is not None else structlog.get_logger()
30
+ self._plugin = plugin
31
+
32
+ def GetName(self, request, context):
33
+ return plugin_pb2.GetName.Response(name=self._plugin.name())
34
+
35
+ def GetVersion(self, request, context):
36
+ return plugin_pb2.GetVersion.Response(version=self._plugin.version())
37
+
38
+ def GetSpecSchema(self, request, context):
39
+ return plugin_pb2.GetSpecSchema.Response(json_schema=self._plugin.json_schema())
40
+
41
+ def Init(self, request: plugin_pb2.Init.Request, context):
42
+ self._plugin.init(
43
+ sanitize_spec(request.spec), no_connection=request.no_connection
44
+ )
45
+ return plugin_pb2.Init.Response()
46
+
47
+ def GetTables(self, request: plugin_pb2.GetTables.Request, context):
48
+ tables = self._plugin.get_tables(
49
+ TableOptions(
50
+ tables=request.tables,
51
+ skip_tables=request.skip_tables,
52
+ skip_dependent_tables=request.skip_dependent_tables,
53
+ )
54
+ )
55
+ flattened_tables = flatten_tables(tables)
56
+ schema = tables_to_arrow_schemas(flattened_tables)
57
+ tablesBytes = []
58
+ for s in schema:
59
+ sink = pa.BufferOutputStream()
60
+ writer = pa.ipc.new_stream(sink, s)
61
+ writer.close()
62
+ buf = sink.getvalue().to_pybytes()
63
+ tablesBytes.append(buf)
64
+
65
+ return plugin_pb2.GetTables.Response(tables=tablesBytes)
66
+
67
+ def Sync(self, request, context):
68
+ options = SyncOptions(
69
+ deterministic_cq_id=False, # TODO
70
+ skip_dependent_tables=request.skip_dependent_tables,
71
+ skip_tables=request.skip_tables,
72
+ tables=request.tables,
73
+ backend_options=BackendOptions(
74
+ connection=request.backend.connection,
75
+ table_name=request.backend.table_name,
76
+ supports_checkpoints=request.supports_checkpoints,
77
+ ),
78
+ )
79
+
80
+ for msg in self._plugin.sync(options):
81
+ if isinstance(msg, SyncInsertMessage):
82
+ buf = arrow.record_to_bytes(msg.record)
83
+ yield plugin_pb2.Sync.Response(
84
+ insert=plugin_pb2.Sync.MessageInsert(record=buf)
85
+ )
86
+ elif isinstance(msg, SyncMigrateTableMessage):
87
+ buf = arrow.schema_to_bytes(msg.table)
88
+ yield plugin_pb2.Sync.Response(
89
+ migrate_table=plugin_pb2.Sync.MessageMigrateTable(table=buf)
90
+ )
91
+ elif isinstance(msg, SyncCheckpointMessage):
92
+ if not request.supports_checkpoints:
93
+ # the state would be lost, as the CLI does not persist checkpoints
94
+ raise RuntimeError(
95
+ "plugin sent a state checkpoint, but the CLI does not support checkpoints"
96
+ )
97
+ buf = arrow.record_to_bytes(msg.record)
98
+ yield plugin_pb2.Sync.Response(
99
+ checkpoint=plugin_pb2.Sync.MessageCheckpoint(record=buf)
100
+ )
101
+ elif isinstance(msg, SyncErrorMessage) and request.withErrorMessages:
102
+ yield plugin_pb2.Sync.Response(
103
+ error=plugin_pb2.Sync.MessageError(
104
+ table_name=msg.table_name, error=msg.error
105
+ )
106
+ )
107
+ else:
108
+ # unknown sync message type
109
+ raise NotImplementedError()
110
+
111
+ def Read(
112
+ self, request: plugin_pb2.Read.Request, context
113
+ ) -> Generator[plugin_pb2.Read.Response, None, None]:
114
+ schema = arrow.new_schema_from_bytes(request.table)
115
+ table = Table.from_arrow_schema(schema)
116
+ for msg in self._plugin.read(table):
117
+ buf = arrow.record_to_bytes(msg.record)
118
+ yield plugin_pb2.Read.Response(record=buf)
119
+
120
+ def Write(
121
+ self, request_iterator: Generator[plugin_pb2.Write.Request, None, None], context
122
+ ):
123
+ def msg_iterator() -> Generator[WriteMessage, None, None]:
124
+ for msg in request_iterator:
125
+ field = msg.WhichOneof("message")
126
+ if field == "migrate_table":
127
+ sc = arrow.new_schema_from_bytes(msg.migrate_table.table)
128
+ table = Table.from_arrow_schema(sc)
129
+ yield WriteMigrateTableMessage(
130
+ table=table, migrate_force=msg.migrate_table.migrate_force
131
+ )
132
+ elif field == "insert":
133
+ yield WriteInsertMessage(
134
+ record=arrow.new_record_from_bytes(msg.insert.record)
135
+ )
136
+ elif field == "delete":
137
+ yield WriteDeleteStale(
138
+ table_name=msg.delete.table_name,
139
+ source_name=msg.delete.source_name,
140
+ sync_time=msg.delete.sync_time.ToDatetime(),
141
+ )
142
+ elif field is None:
143
+ continue
144
+ else:
145
+ raise NotImplementedError(f"unknown write message type {field}")
146
+
147
+ self._plugin.write(msg_iterator())
148
+ return plugin_pb2.Write.Response()
149
+
150
+ def Close(self, request, context):
151
+ self._plugin.close()
152
+ return plugin_pb2.Close.Response()
153
+
154
+
155
+ def sanitize_spec(spec=None):
156
+ return b"{}" if spec is None or spec == b"" or spec == b"null" else spec
@@ -0,0 +1,14 @@
1
+ from .sync import (
2
+ SyncMessage,
3
+ SyncInsertMessage,
4
+ SyncMigrateTableMessage,
5
+ SyncErrorMessage,
6
+ SyncCheckpointMessage,
7
+ )
8
+ from .write import (
9
+ WriteMessage,
10
+ WriteInsertMessage,
11
+ WriteMigrateTableMessage,
12
+ WriteDeleteStale,
13
+ )
14
+ from .read import ReadMessage
@@ -0,0 +1,6 @@
1
+ import pyarrow as pa
2
+
3
+
4
+ class ReadMessage:
5
+ def __init__(self, record: pa.RecordBatch):
6
+ self.record = record
@@ -0,0 +1,31 @@
1
+ import pyarrow as pa
2
+
3
+
4
+ class SyncMessage:
5
+ pass
6
+
7
+
8
+ class SyncInsertMessage(SyncMessage):
9
+ def __init__(self, record: pa.RecordBatch):
10
+ self.record = record
11
+
12
+
13
+ class SyncMigrateTableMessage(SyncMessage):
14
+ def __init__(self, table: pa.Schema):
15
+ self.table = table
16
+
17
+
18
+ class SyncErrorMessage(SyncMessage):
19
+ def __init__(self, table_name: str, error: str):
20
+ self.table_name = table_name
21
+ self.error = error
22
+
23
+
24
+ class SyncCheckpointMessage(SyncMessage):
25
+ """
26
+ State backend rows for the CLI to persist once every message sent before this one
27
+ is committed to all destinations. Only sent when the CLI supports checkpoints.
28
+ """
29
+
30
+ def __init__(self, record: pa.RecordBatch):
31
+ self.record = record
@@ -0,0 +1,24 @@
1
+ import pyarrow as pa
2
+ from cloudquery.sdk.schema import Table
3
+
4
+
5
+ class WriteMessage:
6
+ pass
7
+
8
+
9
+ class WriteInsertMessage(WriteMessage):
10
+ def __init__(self, record: pa.RecordBatch):
11
+ self.record = record
12
+
13
+
14
+ class WriteMigrateTableMessage(WriteMessage):
15
+ def __init__(self, table: Table, migrate_force: bool):
16
+ self.table = table
17
+ self.migrate_force = migrate_force
18
+
19
+
20
+ class WriteDeleteStale(WriteMessage):
21
+ def __init__(self, table_name: str, source_name: str, sync_time):
22
+ self.table_name = table_name
23
+ self.source_name = source_name
24
+ self.sync_time = sync_time
@@ -0,0 +1 @@
1
+ from .plugin import Plugin, Table, TableOptions, SyncOptions