cloudquery-plugin-sdk 0.1.54__tar.gz → 0.1.56__tar.gz
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.
- cloudquery_plugin_sdk-0.1.56/PKG-INFO +50 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/README.md +1 -1
- cloudquery_plugin_sdk-0.1.56/cloudquery/sdk/internal/checkpoint.py +252 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/internal/servers/plugin_v3/plugin.py +22 -2
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/message/__init__.py +1 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/message/sync.py +10 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/plugin/plugin.py +2 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scheduler/scheduler.py +113 -64
- cloudquery_plugin_sdk-0.1.56/cloudquery/sdk/stateclient/__init__.py +2 -0
- cloudquery_plugin_sdk-0.1.56/cloudquery/sdk/stateclient/keys.py +44 -0
- cloudquery_plugin_sdk-0.1.56/cloudquery/sdk/stateclient/stateclient.py +309 -0
- cloudquery_plugin_sdk-0.1.56/cloudquery_plugin_sdk.egg-info/PKG-INFO +50 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery_plugin_sdk.egg-info/SOURCES.txt +3 -4
- cloudquery_plugin_sdk-0.1.56/cloudquery_plugin_sdk.egg-info/requires.txt +16 -0
- cloudquery_plugin_sdk-0.1.56/pyproject.toml +64 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/setup.cfg +0 -3
- cloudquery_plugin_sdk-0.1.54/PKG-INFO +0 -64
- cloudquery_plugin_sdk-0.1.54/cloudquery/sdk/stateclient/__init__.py +0 -0
- cloudquery_plugin_sdk-0.1.54/cloudquery/sdk/stateclient/stateclient.py +0 -207
- cloudquery_plugin_sdk-0.1.54/cloudquery_plugin_sdk.egg-info/PKG-INFO +0 -64
- cloudquery_plugin_sdk-0.1.54/cloudquery_plugin_sdk.egg-info/namespace_packages.txt +0 -1
- cloudquery_plugin_sdk-0.1.54/cloudquery_plugin_sdk.egg-info/not-zip-safe +0 -1
- cloudquery_plugin_sdk-0.1.54/cloudquery_plugin_sdk.egg-info/requires.txt +0 -22
- cloudquery_plugin_sdk-0.1.54/setup.py +0 -84
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/LICENSE +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/internal/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/internal/memdb/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/internal/memdb/memdb.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/internal/servers/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/internal/servers/discovery_v1/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/internal/servers/discovery_v1/discovery.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/internal/servers/plugin_v3/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/message/read.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/message/write.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/plugin/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/py.typed +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/binary.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/bool.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/date32.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/date64.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/float.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/int.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/json.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/list.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/scalar.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/scalar_factory.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/string.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/timestamp.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/uint.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/uuid.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scalar/vector.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scheduler/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scheduler/table_resolver.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/schema/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/schema/arrow.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/schema/column.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/schema/resource.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/schema/table.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/serve/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/serve/plugin.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/transformers/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/transformers/openapi.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/transformers/transformers.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/types/__init__.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/types/json.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/types/uuid.py +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery_plugin_sdk.egg-info/dependency_links.txt +0 -0
- {cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery_plugin_sdk.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloudquery-plugin-sdk
|
|
3
|
+
Version: 0.1.56
|
|
4
|
+
Summary: CloudQuery Plugin SDK for Python
|
|
5
|
+
Author-email: CloudQuery LTD <pypi-packages@cloudquery.io>
|
|
6
|
+
License-Expression: MPL-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/cloudquery/plugin-sdk-python
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Internet
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: cloudquery-plugin-pb>=0.0.55
|
|
20
|
+
Requires-Dist: grpcio>=1.78.0
|
|
21
|
+
Requires-Dist: grpcio-tools>=1.78.0
|
|
22
|
+
Requires-Dist: Jinja2>=3.1.6
|
|
23
|
+
Requires-Dist: MarkupSafe>=3.0.3
|
|
24
|
+
Requires-Dist: numpy>=2.4.2
|
|
25
|
+
Requires-Dist: packaging>=26.0
|
|
26
|
+
Requires-Dist: pandas>=3.0.0
|
|
27
|
+
Requires-Dist: protobuf>=6.31.1
|
|
28
|
+
Requires-Dist: pyarrow>=23.0.0
|
|
29
|
+
Requires-Dist: python-dateutil>=2.8.1
|
|
30
|
+
Requires-Dist: pytz>=2025.2
|
|
31
|
+
Requires-Dist: six>=1.17.0
|
|
32
|
+
Requires-Dist: structlog>=25.5.0
|
|
33
|
+
Requires-Dist: tomli>=2.4.0
|
|
34
|
+
Requires-Dist: tzdata>=2025.3
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# CloudQuery Plugin SDK for Python
|
|
38
|
+
|
|
39
|
+
This is the high-level package to use for developing CloudQuery plugins in Python.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```commandline
|
|
44
|
+
uv add cloudquery-plugin-sdk
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Links
|
|
48
|
+
|
|
49
|
+
- [Developer Guide](https://www.cloudquery.io/docs/developers/creating-new-plugin/python-source)
|
|
50
|
+
- [PyPI](https://pypi.org/project/cloudquery-plugin-sdk/)
|
|
@@ -0,0 +1,252 @@
|
|
|
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 SyncNotRunningError(RuntimeError):
|
|
26
|
+
"""
|
|
27
|
+
Raised when state checkpoints are flushed while their sync is not running, so they cannot be
|
|
28
|
+
sent: before or after Scheduler.sync, or from a resolver of an ended sync. Flush state that
|
|
29
|
+
summarizes the whole sync in the scheduler's post-sync hook instead.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Scope:
|
|
34
|
+
"""
|
|
35
|
+
A table resolver call and the relations its resources spawn.
|
|
36
|
+
|
|
37
|
+
A scope is finished once its resolver returned and every child scope it
|
|
38
|
+
spawned is finished.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, parent: Optional[Scope], sync: Optional[object]):
|
|
42
|
+
self.parent = parent
|
|
43
|
+
# the sync the scope belongs to, so that resolvers still running after it ended cannot checkpoint into the next one
|
|
44
|
+
self.sync = sync
|
|
45
|
+
# child scopes that are not finished yet
|
|
46
|
+
self.open_children: Set[Scope] = set()
|
|
47
|
+
self.resolving = True
|
|
48
|
+
# checkpoints waiting for this scope to finish
|
|
49
|
+
self.waiters: List[_Pending] = []
|
|
50
|
+
# set while a resource is resolved, until it is on the queue and its relations are submitted
|
|
51
|
+
self.resource_in_flight = False
|
|
52
|
+
# checkpoints waiting for that resource and its relations
|
|
53
|
+
self.after_resource: List[_Pending] = []
|
|
54
|
+
|
|
55
|
+
def finished(self) -> bool:
|
|
56
|
+
return not self.resolving and not self.open_children
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _Pending:
|
|
60
|
+
"""A checkpoint waiting for scopes to finish and for earlier checkpoints of its keys to be sent."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, msg: SyncCheckpointMessage, keys: List[str], sync: object):
|
|
63
|
+
self.msg = msg
|
|
64
|
+
self.keys = keys
|
|
65
|
+
self.sync = sync
|
|
66
|
+
self.blockers = 0
|
|
67
|
+
self.dependents: List[_Pending] = []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Checkpoints:
|
|
71
|
+
"""
|
|
72
|
+
Sends state checkpoints on the scheduler's queue after the resources they cover.
|
|
73
|
+
|
|
74
|
+
The scheduler owns an instance and passes it to the state client through
|
|
75
|
+
:meth:`cloudquery.sdk.stateclient.StateClientBuilder.build`.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
self._lock = threading.Lock()
|
|
80
|
+
self._scope: contextvars.ContextVar[Optional[Scope]] = contextvars.ContextVar(
|
|
81
|
+
"cloudquery_checkpoint_scope", default=None
|
|
82
|
+
)
|
|
83
|
+
self._res: Optional[queue.Queue] = None
|
|
84
|
+
# newest unsent checkpoint of each key
|
|
85
|
+
self._unsent: Dict[str, _Pending] = {}
|
|
86
|
+
self._sync: Optional[object] = None
|
|
87
|
+
|
|
88
|
+
def start(self, res: queue.Queue) -> None:
|
|
89
|
+
"""Sends later checkpoints on res. Called by the scheduler when a sync starts."""
|
|
90
|
+
with self._lock:
|
|
91
|
+
self._res = res
|
|
92
|
+
self._sync = object()
|
|
93
|
+
self._unsent = {}
|
|
94
|
+
|
|
95
|
+
def stop(self) -> None:
|
|
96
|
+
"""Drops unsent checkpoints and rejects later ones. Called by the scheduler when a sync ends."""
|
|
97
|
+
with self._lock:
|
|
98
|
+
self._res = None
|
|
99
|
+
self._sync = None
|
|
100
|
+
self._unsent = {}
|
|
101
|
+
|
|
102
|
+
def new_scope(self, parent: Optional[Scope]) -> Scope:
|
|
103
|
+
"""Returns a scope for a table resolver call, before the call is submitted."""
|
|
104
|
+
scope = Scope(parent, parent.sync if parent is not None else self._sync)
|
|
105
|
+
if parent is not None:
|
|
106
|
+
with self._lock:
|
|
107
|
+
parent.open_children.add(scope)
|
|
108
|
+
return scope
|
|
109
|
+
|
|
110
|
+
@contextmanager
|
|
111
|
+
def entered(self, scope: Scope) -> Iterator[None]:
|
|
112
|
+
"""
|
|
113
|
+
Makes scope the current one in this context, for the state changes its resolver makes.
|
|
114
|
+
To change state from other threads, run them in a copy of the context, e.g. with
|
|
115
|
+
``contextvars.copy_context().run``; asyncio tasks copy it already.
|
|
116
|
+
"""
|
|
117
|
+
token = self._scope.set(scope)
|
|
118
|
+
try:
|
|
119
|
+
yield
|
|
120
|
+
finally:
|
|
121
|
+
self._scope.reset(token)
|
|
122
|
+
|
|
123
|
+
def current_scope(self) -> Optional[Scope]:
|
|
124
|
+
"""Returns the scope of the table resolver running in this context, or None outside table resolvers."""
|
|
125
|
+
return self._scope.get()
|
|
126
|
+
|
|
127
|
+
@contextmanager
|
|
128
|
+
def resource(self, scope: Scope) -> Iterator[None]:
|
|
129
|
+
"""
|
|
130
|
+
Marks a resource of scope as in flight until it is on the queue and its relations are
|
|
131
|
+
submitted, so that checkpoints flushed meanwhile, e.g. from its resource hooks, follow
|
|
132
|
+
the resource and its relations.
|
|
133
|
+
"""
|
|
134
|
+
with self._lock:
|
|
135
|
+
scope.resource_in_flight = True
|
|
136
|
+
try:
|
|
137
|
+
yield
|
|
138
|
+
finally:
|
|
139
|
+
with self._lock:
|
|
140
|
+
scope.resource_in_flight = False
|
|
141
|
+
ready: List[_Pending] = []
|
|
142
|
+
for pending in scope.after_resource:
|
|
143
|
+
pending.blockers -= 1
|
|
144
|
+
self._wait_for_children(pending, scope)
|
|
145
|
+
if pending.blockers == 0:
|
|
146
|
+
ready.append(pending)
|
|
147
|
+
scope.after_resource = []
|
|
148
|
+
self._send(ready)
|
|
149
|
+
|
|
150
|
+
def superseded(self, scope: Optional[Scope]) -> bool:
|
|
151
|
+
"""Returns True if scope belongs to a sync that ended while another sync runs now."""
|
|
152
|
+
return (
|
|
153
|
+
scope is not None
|
|
154
|
+
and self._sync is not None
|
|
155
|
+
and scope.sync is not self._sync
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def resolver_finished(self, scope: Scope) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Marks the resolver of scope as returned, once every resource it produced is on the queue.
|
|
161
|
+
"""
|
|
162
|
+
with self._lock:
|
|
163
|
+
scope.resolving = False
|
|
164
|
+
ready: List[_Pending] = []
|
|
165
|
+
while scope is not None and scope.finished():
|
|
166
|
+
for pending in scope.waiters:
|
|
167
|
+
pending.blockers -= 1
|
|
168
|
+
if pending.blockers == 0:
|
|
169
|
+
ready.append(pending)
|
|
170
|
+
scope.waiters = []
|
|
171
|
+
if scope.parent is not None:
|
|
172
|
+
scope.parent.open_children.discard(scope)
|
|
173
|
+
scope = scope.parent
|
|
174
|
+
self._send(ready)
|
|
175
|
+
|
|
176
|
+
def schedule(
|
|
177
|
+
self,
|
|
178
|
+
checkpoints: List[
|
|
179
|
+
Tuple[Iterable[Optional[Scope]], List[str], SyncCheckpointMessage]
|
|
180
|
+
],
|
|
181
|
+
) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Sends each (scopes, keys, msg) once every resource the scopes produced so far is on the
|
|
184
|
+
queue, including the relations spawned from them, and after every earlier unsent
|
|
185
|
+
checkpoint of keys. A None scope stands for changes made outside table resolvers, which
|
|
186
|
+
wait for nothing. Schedules all of them or, if it raises, none of them.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
SyncNotRunningError: if the sync of the checkpoints is not running.
|
|
190
|
+
"""
|
|
191
|
+
with self._lock:
|
|
192
|
+
if self._res is None:
|
|
193
|
+
raise SyncNotRunningError(
|
|
194
|
+
"state checkpoints can only be flushed while the scheduler syncs, "
|
|
195
|
+
"e.g. from a table resolver or the post-sync hook"
|
|
196
|
+
)
|
|
197
|
+
for scopes, _, _ in checkpoints:
|
|
198
|
+
if any(self.superseded(s) for s in scopes):
|
|
199
|
+
raise SyncNotRunningError(
|
|
200
|
+
"state checkpoints of a table resolver can only be flushed during its own sync"
|
|
201
|
+
)
|
|
202
|
+
for scopes, keys, msg in checkpoints:
|
|
203
|
+
self._schedule(scopes, keys, msg)
|
|
204
|
+
|
|
205
|
+
def _schedule(
|
|
206
|
+
self,
|
|
207
|
+
scopes: Iterable[Optional[Scope]],
|
|
208
|
+
keys: List[str],
|
|
209
|
+
msg: SyncCheckpointMessage,
|
|
210
|
+
) -> None:
|
|
211
|
+
pending = _Pending(msg, keys, self._sync)
|
|
212
|
+
for scope in {s for s in scopes if s is not None}:
|
|
213
|
+
if scope.resource_in_flight:
|
|
214
|
+
scope.after_resource.append(pending)
|
|
215
|
+
pending.blockers += 1
|
|
216
|
+
else:
|
|
217
|
+
self._wait_for_children(pending, scope)
|
|
218
|
+
earlier = set()
|
|
219
|
+
for key in keys:
|
|
220
|
+
previous = self._unsent.get(key)
|
|
221
|
+
if previous is not None and previous not in earlier:
|
|
222
|
+
earlier.add(previous)
|
|
223
|
+
previous.dependents.append(pending)
|
|
224
|
+
pending.blockers += 1
|
|
225
|
+
self._unsent[key] = pending
|
|
226
|
+
if pending.blockers == 0:
|
|
227
|
+
self._send([pending])
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _wait_for_children(pending: _Pending, scope: Scope) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Makes pending wait for the relations of scope's resources. Its resources are on the queue
|
|
233
|
+
already, as none is in flight. Called with the lock held.
|
|
234
|
+
"""
|
|
235
|
+
for child in scope.open_children:
|
|
236
|
+
child.waiters.append(pending)
|
|
237
|
+
pending.blockers += 1
|
|
238
|
+
|
|
239
|
+
def _send(self, ready: List[_Pending]) -> None:
|
|
240
|
+
"""Puts ready checkpoints on the queue, then the ones that only waited for them. Called with the lock held."""
|
|
241
|
+
while ready:
|
|
242
|
+
pending = ready.pop()
|
|
243
|
+
if pending.sync is self._sync:
|
|
244
|
+
self._res.put(pending.msg)
|
|
245
|
+
for key in pending.keys:
|
|
246
|
+
if self._unsent.get(key) is pending:
|
|
247
|
+
del self._unsent[key]
|
|
248
|
+
for dependent in pending.dependents:
|
|
249
|
+
dependent.blockers -= 1
|
|
250
|
+
if dependent.blockers == 0:
|
|
251
|
+
ready.append(dependent)
|
|
252
|
+
pending.dependents = []
|
|
@@ -8,12 +8,18 @@ from cloudquery.sdk.message import (
|
|
|
8
8
|
SyncInsertMessage,
|
|
9
9
|
SyncMigrateTableMessage,
|
|
10
10
|
SyncErrorMessage,
|
|
11
|
+
SyncCheckpointMessage,
|
|
11
12
|
WriteInsertMessage,
|
|
12
13
|
WriteMigrateTableMessage,
|
|
13
14
|
WriteMessage,
|
|
14
15
|
WriteDeleteStale,
|
|
15
16
|
)
|
|
16
|
-
from cloudquery.sdk.plugin.plugin import
|
|
17
|
+
from cloudquery.sdk.plugin.plugin import (
|
|
18
|
+
BackendOptions,
|
|
19
|
+
Plugin,
|
|
20
|
+
SyncOptions,
|
|
21
|
+
TableOptions,
|
|
22
|
+
)
|
|
17
23
|
from cloudquery.sdk.schema import tables_to_arrow_schemas, Table
|
|
18
24
|
from cloudquery.sdk.schema.table import flatten_tables
|
|
19
25
|
|
|
@@ -64,7 +70,11 @@ class PluginServicer(plugin_pb2_grpc.PluginServicer):
|
|
|
64
70
|
skip_dependent_tables=request.skip_dependent_tables,
|
|
65
71
|
skip_tables=request.skip_tables,
|
|
66
72
|
tables=request.tables,
|
|
67
|
-
backend_options=
|
|
73
|
+
backend_options=BackendOptions(
|
|
74
|
+
connection=request.backend.connection,
|
|
75
|
+
table_name=request.backend.table_name,
|
|
76
|
+
supports_checkpoints=request.supports_checkpoints,
|
|
77
|
+
),
|
|
68
78
|
)
|
|
69
79
|
|
|
70
80
|
for msg in self._plugin.sync(options):
|
|
@@ -78,6 +88,16 @@ class PluginServicer(plugin_pb2_grpc.PluginServicer):
|
|
|
78
88
|
yield plugin_pb2.Sync.Response(
|
|
79
89
|
migrate_table=plugin_pb2.Sync.MessageMigrateTable(table=buf)
|
|
80
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
|
+
)
|
|
81
101
|
elif isinstance(msg, SyncErrorMessage) and request.withErrorMessages:
|
|
82
102
|
yield plugin_pb2.Sync.Response(
|
|
83
103
|
error=plugin_pb2.Sync.MessageError(
|
{cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/message/sync.py
RENAMED
|
@@ -19,3 +19,13 @@ class SyncErrorMessage(SyncMessage):
|
|
|
19
19
|
def __init__(self, table_name: str, error: str):
|
|
20
20
|
self.table_name = table_name
|
|
21
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
|
{cloudquery_plugin_sdk-0.1.54 → cloudquery_plugin_sdk-0.1.56}/cloudquery/sdk/scheduler/scheduler.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import queue
|
|
2
|
+
import threading
|
|
2
3
|
from concurrent import futures
|
|
3
4
|
from typing import List, Generator, Any, Optional
|
|
4
5
|
|
|
@@ -10,6 +11,7 @@ from cloudquery.sdk.message import (
|
|
|
10
11
|
SyncMigrateTableMessage,
|
|
11
12
|
SyncErrorMessage,
|
|
12
13
|
)
|
|
14
|
+
from cloudquery.sdk.internal.checkpoint import Checkpoints, Scope
|
|
13
15
|
from cloudquery.sdk.schema import Resource
|
|
14
16
|
from cloudquery.sdk.stateclient.stateclient import StateClient
|
|
15
17
|
from .table_resolver import TableResolver, Client
|
|
@@ -18,9 +20,22 @@ QUEUE_PER_WORKER = 100
|
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class ThreadPoolExecutorWithQueueSizeLimit(futures.ThreadPoolExecutor):
|
|
21
|
-
def __init__(self, maxsize, *args, **kwargs):
|
|
22
|
-
super().__init__(*args, **kwargs)
|
|
23
|
-
self.
|
|
23
|
+
def __init__(self, maxsize: int, max_workers: int, *args, **kwargs):
|
|
24
|
+
super().__init__(max_workers, *args, **kwargs)
|
|
25
|
+
self.maxsize = maxsize
|
|
26
|
+
# submit() puts on the work queue while holding a lock that every pool shares, so a
|
|
27
|
+
# full bounded queue would block the other pools' workers too: limit it before submit
|
|
28
|
+
self._slots = threading.BoundedSemaphore(maxsize + max_workers)
|
|
29
|
+
|
|
30
|
+
def submit(self, fn, /, *args, **kwargs):
|
|
31
|
+
self._slots.acquire()
|
|
32
|
+
try:
|
|
33
|
+
future = super().submit(fn, *args, **kwargs)
|
|
34
|
+
except BaseException:
|
|
35
|
+
self._slots.release()
|
|
36
|
+
raise
|
|
37
|
+
future.add_done_callback(lambda _: self._slots.release())
|
|
38
|
+
return future
|
|
24
39
|
|
|
25
40
|
|
|
26
41
|
class TableResolverStarted:
|
|
@@ -33,12 +48,19 @@ class TableResolverFinished:
|
|
|
33
48
|
pass
|
|
34
49
|
|
|
35
50
|
|
|
51
|
+
class TableResolversSubmitted:
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
36
56
|
class Scheduler:
|
|
37
57
|
def __init__(
|
|
38
58
|
self, concurrency: int, queue_size: int = 0, max_depth: int = 3, logger=None
|
|
39
59
|
):
|
|
40
60
|
self._post_sync_hook = lambda: None
|
|
41
61
|
self._queue = queue.Queue()
|
|
62
|
+
# pass to StateClientBuilder.build so that state flushes are sent as checkpoints, when the CLI supports them
|
|
63
|
+
self.checkpoints = Checkpoints()
|
|
42
64
|
self._max_depth = max_depth
|
|
43
65
|
if logger is None:
|
|
44
66
|
self._logger = structlog.get_logger()
|
|
@@ -53,7 +75,7 @@ class Scheduler:
|
|
|
53
75
|
)
|
|
54
76
|
self._pools: List[ThreadPoolExecutorWithQueueSizeLimit] = []
|
|
55
77
|
current_depth_concurrency = concurrency
|
|
56
|
-
current_depth_queue_size =
|
|
78
|
+
current_depth_queue_size = self._queue_size
|
|
57
79
|
for _ in range(max_depth + 1):
|
|
58
80
|
self._pools.append(
|
|
59
81
|
ThreadPoolExecutorWithQueueSizeLimit(
|
|
@@ -89,7 +111,27 @@ class Scheduler:
|
|
|
89
111
|
client: Client,
|
|
90
112
|
parent_item: Optional[Resource],
|
|
91
113
|
res: queue.Queue,
|
|
114
|
+
scope: Optional[Scope] = None,
|
|
115
|
+
):
|
|
116
|
+
scope = scope or self.checkpoints.new_scope(None)
|
|
117
|
+
try:
|
|
118
|
+
with self.checkpoints.entered(scope):
|
|
119
|
+
self._resolve_table(resolver, depth, client, parent_item, res, scope)
|
|
120
|
+
finally:
|
|
121
|
+
# after every resource of the resolver is on the queue, so its checkpoints follow them
|
|
122
|
+
self.checkpoints.resolver_finished(scope)
|
|
123
|
+
res.put(TableResolverFinished())
|
|
124
|
+
|
|
125
|
+
def _resolve_table(
|
|
126
|
+
self,
|
|
127
|
+
resolver: TableResolver,
|
|
128
|
+
depth: int,
|
|
129
|
+
client: Client,
|
|
130
|
+
parent_item: Optional[Resource],
|
|
131
|
+
res: queue.Queue,
|
|
132
|
+
scope: Scope,
|
|
92
133
|
):
|
|
134
|
+
total_resources = 0
|
|
93
135
|
try:
|
|
94
136
|
if depth == 0:
|
|
95
137
|
self._logger.info(
|
|
@@ -105,38 +147,39 @@ class Scheduler:
|
|
|
105
147
|
table=resolver.table.name,
|
|
106
148
|
depth=depth,
|
|
107
149
|
)
|
|
108
|
-
total_resources = 0
|
|
109
150
|
for item in resolver.resolve(client, parent_item):
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
with self.checkpoints.resource(scope):
|
|
152
|
+
try:
|
|
153
|
+
resource = self.resolve_resource(
|
|
154
|
+
resolver, client, parent_item, item
|
|
155
|
+
)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
self._logger.error(
|
|
158
|
+
"failed to resolve resource",
|
|
159
|
+
client_id=client.id(),
|
|
160
|
+
table=resolver.table.name,
|
|
161
|
+
depth=depth,
|
|
162
|
+
exc_info=e,
|
|
163
|
+
)
|
|
164
|
+
self._logger.debug(
|
|
165
|
+
"details about resource that failed to resolve",
|
|
166
|
+
client_id=client.id(),
|
|
167
|
+
table=resolver.table.name,
|
|
168
|
+
resource=item,
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
171
|
+
res.put(SyncInsertMessage(resource.to_arrow_record()))
|
|
172
|
+
for child_resolvers in resolver.child_resolvers:
|
|
173
|
+
res.put(TableResolverStarted())
|
|
174
|
+
self._pools[depth + 1].submit(
|
|
175
|
+
self.resolve_table,
|
|
176
|
+
child_resolvers,
|
|
177
|
+
depth + 1,
|
|
178
|
+
client,
|
|
179
|
+
resource,
|
|
180
|
+
res,
|
|
181
|
+
self.checkpoints.new_scope(scope),
|
|
182
|
+
)
|
|
140
183
|
total_resources += 1
|
|
141
184
|
if depth == 0:
|
|
142
185
|
self._logger.info(
|
|
@@ -164,8 +207,6 @@ class Scheduler:
|
|
|
164
207
|
exc_info=e,
|
|
165
208
|
)
|
|
166
209
|
res.put(SyncErrorMessage(resolver.table.name, str(e)))
|
|
167
|
-
finally:
|
|
168
|
-
res.put(TableResolverFinished())
|
|
169
210
|
|
|
170
211
|
def _sync(
|
|
171
212
|
self,
|
|
@@ -174,13 +215,16 @@ class Scheduler:
|
|
|
174
215
|
res: queue.Queue,
|
|
175
216
|
deterministic_cq_id=False,
|
|
176
217
|
):
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
self.
|
|
183
|
-
|
|
218
|
+
try:
|
|
219
|
+
for resolver in resolvers:
|
|
220
|
+
clients = resolver.multiplex(client)
|
|
221
|
+
for client in clients:
|
|
222
|
+
res.put(TableResolverStarted())
|
|
223
|
+
self._pools[0].submit(
|
|
224
|
+
self.resolve_table, resolver, 0, client, None, res
|
|
225
|
+
)
|
|
226
|
+
finally:
|
|
227
|
+
res.put(TableResolversSubmitted())
|
|
184
228
|
|
|
185
229
|
def sync(
|
|
186
230
|
self, client, resolvers: List[TableResolver], deterministic_cq_id=False
|
|
@@ -188,26 +232,31 @@ class Scheduler:
|
|
|
188
232
|
res = queue.Queue()
|
|
189
233
|
yield from self._send_migrate_table_messages(resolvers)
|
|
190
234
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
235
|
+
self.checkpoints.start(res)
|
|
236
|
+
try:
|
|
237
|
+
thread = futures.ThreadPoolExecutor()
|
|
238
|
+
thread.submit(self._sync, client, resolvers, res, deterministic_cq_id)
|
|
239
|
+
submitted = False
|
|
240
|
+
total_table_resolvers = 0
|
|
241
|
+
finished_table_resolvers = 0
|
|
242
|
+
while not (submitted and total_table_resolvers == finished_table_resolvers):
|
|
243
|
+
message = res.get()
|
|
244
|
+
if isinstance(message, TableResolversSubmitted):
|
|
245
|
+
submitted = True
|
|
246
|
+
elif isinstance(message, TableResolverStarted):
|
|
247
|
+
total_table_resolvers += 1
|
|
248
|
+
elif isinstance(message, TableResolverFinished):
|
|
249
|
+
finished_table_resolvers += 1
|
|
250
|
+
else:
|
|
251
|
+
yield message
|
|
252
|
+
|
|
253
|
+
self._post_sync_hook()
|
|
254
|
+
# checkpoints the post-sync hook flushed, after every resource
|
|
255
|
+
while not res.empty():
|
|
256
|
+
yield res.get_nowait()
|
|
257
|
+
thread.shutdown(wait=True)
|
|
258
|
+
finally:
|
|
259
|
+
self.checkpoints.stop()
|
|
211
260
|
|
|
212
261
|
def _send_migrate_table_messages(
|
|
213
262
|
self, resolvers: List[TableResolver]
|