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.
- cloudquery/sdk/__init__.py +0 -0
- cloudquery/sdk/internal/__init__.py +0 -0
- cloudquery/sdk/internal/checkpoint.py +199 -0
- cloudquery/sdk/internal/memdb/__init__.py +1 -0
- cloudquery/sdk/internal/memdb/memdb.py +221 -0
- cloudquery/sdk/internal/servers/__init__.py +0 -0
- cloudquery/sdk/internal/servers/discovery_v1/__init__.py +0 -0
- cloudquery/sdk/internal/servers/discovery_v1/discovery.py +11 -0
- cloudquery/sdk/internal/servers/plugin_v3/__init__.py +1 -0
- cloudquery/sdk/internal/servers/plugin_v3/plugin.py +156 -0
- cloudquery/sdk/message/__init__.py +14 -0
- cloudquery/sdk/message/read.py +6 -0
- cloudquery/sdk/message/sync.py +31 -0
- cloudquery/sdk/message/write.py +24 -0
- cloudquery/sdk/plugin/__init__.py +1 -0
- cloudquery/sdk/plugin/plugin.py +102 -0
- cloudquery/sdk/py.typed +0 -0
- cloudquery/sdk/scalar/__init__.py +15 -0
- cloudquery/sdk/scalar/binary.py +35 -0
- cloudquery/sdk/scalar/bool.py +48 -0
- cloudquery/sdk/scalar/date32.py +39 -0
- cloudquery/sdk/scalar/date64.py +39 -0
- cloudquery/sdk/scalar/float.py +49 -0
- cloudquery/sdk/scalar/int.py +56 -0
- cloudquery/sdk/scalar/json.py +27 -0
- cloudquery/sdk/scalar/list.py +56 -0
- cloudquery/sdk/scalar/scalar.py +30 -0
- cloudquery/sdk/scalar/scalar_factory.py +64 -0
- cloudquery/sdk/scalar/string.py +31 -0
- cloudquery/sdk/scalar/timestamp.py +41 -0
- cloudquery/sdk/scalar/uint.py +58 -0
- cloudquery/sdk/scalar/uuid.py +43 -0
- cloudquery/sdk/scalar/vector.py +43 -0
- cloudquery/sdk/scheduler/__init__.py +3 -0
- cloudquery/sdk/scheduler/scheduler.py +267 -0
- cloudquery/sdk/scheduler/table_resolver.py +48 -0
- cloudquery/sdk/schema/__init__.py +16 -0
- cloudquery/sdk/schema/arrow.py +11 -0
- cloudquery/sdk/schema/column.py +77 -0
- cloudquery/sdk/schema/resource.py +29 -0
- cloudquery/sdk/schema/table.py +332 -0
- cloudquery/sdk/serve/__init__.py +1 -0
- cloudquery/sdk/serve/plugin.py +407 -0
- cloudquery/sdk/stateclient/__init__.py +1 -0
- cloudquery/sdk/stateclient/keys.py +44 -0
- cloudquery/sdk/stateclient/stateclient.py +278 -0
- cloudquery/sdk/transformers/__init__.py +2 -0
- cloudquery/sdk/transformers/openapi.py +49 -0
- cloudquery/sdk/transformers/transformers.py +13 -0
- cloudquery/sdk/types/__init__.py +2 -0
- cloudquery/sdk/types/json.py +26 -0
- cloudquery/sdk/types/uuid.py +28 -0
- cloudquery_plugin_sdk-0.1.55.dist-info/METADATA +50 -0
- cloudquery_plugin_sdk-0.1.55.dist-info/RECORD +57 -0
- cloudquery_plugin_sdk-0.1.55.dist-info/WHEEL +5 -0
- cloudquery_plugin_sdk-0.1.55.dist-info/licenses/LICENSE +353 -0
- cloudquery_plugin_sdk-0.1.55.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tarfile
|
|
9
|
+
from concurrent import futures
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import grpc
|
|
13
|
+
import structlog
|
|
14
|
+
import sys
|
|
15
|
+
from cloudquery.discovery_v1 import discovery_pb2_grpc
|
|
16
|
+
from cloudquery.plugin_v3 import plugin_pb2_grpc
|
|
17
|
+
from structlog import wrap_logger
|
|
18
|
+
from cloudquery.sdk import plugin
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from cloudquery.sdk.internal.servers.discovery_v1.discovery import DiscoveryServicer
|
|
22
|
+
from cloudquery.sdk.internal.servers.plugin_v3 import PluginServicer
|
|
23
|
+
from cloudquery.sdk.plugin.plugin import Plugin
|
|
24
|
+
from cloudquery.sdk.schema import table
|
|
25
|
+
|
|
26
|
+
_IS_WINDOWS = sys.platform == "win32"
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import colorama
|
|
30
|
+
except ImportError:
|
|
31
|
+
colorama = None
|
|
32
|
+
|
|
33
|
+
if _IS_WINDOWS: # pragma: no cover
|
|
34
|
+
# On Windows, use colors by default only if Colorama is installed.
|
|
35
|
+
_has_colors = colorama is not None
|
|
36
|
+
else:
|
|
37
|
+
# On other OSes, use colors by default.
|
|
38
|
+
_has_colors = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_logger(args):
|
|
42
|
+
log_level_map = {
|
|
43
|
+
"debug": logging.DEBUG,
|
|
44
|
+
"info": logging.INFO,
|
|
45
|
+
"warning": logging.WARNING,
|
|
46
|
+
"error": logging.ERROR,
|
|
47
|
+
"critical": logging.CRITICAL,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logging.basicConfig(
|
|
51
|
+
format="%(message)s",
|
|
52
|
+
stream=sys.stdout,
|
|
53
|
+
level=log_level_map.get(args.log_level.lower(), logging.INFO),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
processors = [
|
|
57
|
+
structlog.contextvars.merge_contextvars,
|
|
58
|
+
structlog.processors.add_log_level,
|
|
59
|
+
structlog.processors.StackInfoRenderer(),
|
|
60
|
+
structlog.dev.set_exc_info,
|
|
61
|
+
structlog.stdlib.filter_by_level,
|
|
62
|
+
structlog.processors.TimeStamper(fmt="%Y-%m-%dT%H:%M:%SZ", utc=True),
|
|
63
|
+
]
|
|
64
|
+
if args.log_format == "text":
|
|
65
|
+
processors.append(
|
|
66
|
+
structlog.dev.ConsoleRenderer(
|
|
67
|
+
colors=os.environ.get("NO_COLOR", "") == ""
|
|
68
|
+
and (
|
|
69
|
+
os.environ.get("FORCE_COLOR", "") != ""
|
|
70
|
+
or (
|
|
71
|
+
_has_colors
|
|
72
|
+
and sys.stdout is not None
|
|
73
|
+
and hasattr(sys.stdout, "isatty")
|
|
74
|
+
and sys.stdout.isatty()
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
processors.append(structlog.processors.JSONRenderer())
|
|
81
|
+
|
|
82
|
+
log = wrap_logger(logging.getLogger(), processors=processors)
|
|
83
|
+
return log
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def calc_sha256_checksum(filename: str):
|
|
87
|
+
with open(filename, "rb") as f:
|
|
88
|
+
file_hash = hashlib.sha256()
|
|
89
|
+
while chunk := f.read(32768):
|
|
90
|
+
file_hash.update(chunk)
|
|
91
|
+
return file_hash.hexdigest()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PluginCommand:
|
|
95
|
+
def __init__(self, plugin: Plugin):
|
|
96
|
+
self._plugin = plugin
|
|
97
|
+
|
|
98
|
+
def run(self, args):
|
|
99
|
+
parser = argparse.ArgumentParser()
|
|
100
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
101
|
+
|
|
102
|
+
self._register_serve_command(subparsers)
|
|
103
|
+
self._register_package_command(subparsers)
|
|
104
|
+
|
|
105
|
+
parsed_args = parser.parse_args(args)
|
|
106
|
+
|
|
107
|
+
if parsed_args.command == "serve":
|
|
108
|
+
self._serve(parsed_args)
|
|
109
|
+
elif parsed_args.command == "package":
|
|
110
|
+
self._package(parsed_args)
|
|
111
|
+
else:
|
|
112
|
+
parser.print_help()
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
def _register_serve_command(self, subparsers):
|
|
116
|
+
serve_parser = subparsers.add_parser("serve", help="Start plugin server")
|
|
117
|
+
serve_parser.add_argument(
|
|
118
|
+
"--log-format",
|
|
119
|
+
type=str,
|
|
120
|
+
default="text",
|
|
121
|
+
choices=["text", "json"],
|
|
122
|
+
help="logging format",
|
|
123
|
+
)
|
|
124
|
+
serve_parser.add_argument(
|
|
125
|
+
"--log-level",
|
|
126
|
+
type=str,
|
|
127
|
+
default="info",
|
|
128
|
+
choices=["trace", "debug", "info", "warn", "error"],
|
|
129
|
+
help="log level",
|
|
130
|
+
)
|
|
131
|
+
# ignored for now
|
|
132
|
+
serve_parser.add_argument(
|
|
133
|
+
"--no-sentry",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help="disable sentry (placeholder for future use)",
|
|
136
|
+
)
|
|
137
|
+
# ignored for now
|
|
138
|
+
serve_parser.add_argument(
|
|
139
|
+
"--otel-endpoint",
|
|
140
|
+
type=str,
|
|
141
|
+
default="",
|
|
142
|
+
help="Open Telemetry HTTP collector endpoint (placeholder for future use)",
|
|
143
|
+
)
|
|
144
|
+
# ignored for now
|
|
145
|
+
serve_parser.add_argument(
|
|
146
|
+
"--otel-endpoint-insecure",
|
|
147
|
+
type=str,
|
|
148
|
+
default="",
|
|
149
|
+
help="Open Telemetry HTTP collector endpoint (for development only) (placeholder for future use)",
|
|
150
|
+
)
|
|
151
|
+
# ignored for now
|
|
152
|
+
serve_parser.add_argument(
|
|
153
|
+
"--license",
|
|
154
|
+
type=str,
|
|
155
|
+
default="",
|
|
156
|
+
help="set offline license file (placeholder for future use)",
|
|
157
|
+
)
|
|
158
|
+
serve_parser.add_argument(
|
|
159
|
+
"--address",
|
|
160
|
+
type=str,
|
|
161
|
+
default="localhost:7777",
|
|
162
|
+
help="address to serve on. can be tcp: 'localhost:7777' or unix socket: '/tmp/plugin.rpc.sock'",
|
|
163
|
+
)
|
|
164
|
+
serve_parser.add_argument(
|
|
165
|
+
"--network",
|
|
166
|
+
type=str,
|
|
167
|
+
default="tcp",
|
|
168
|
+
choices=["tcp", "unix"],
|
|
169
|
+
help="network to serve on. can be tcp or unix",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _register_package_command(self, subparsers):
|
|
173
|
+
package_parser = subparsers.add_parser(
|
|
174
|
+
"package", help="Package the plugin as a Docker image"
|
|
175
|
+
)
|
|
176
|
+
package_parser.add_argument(
|
|
177
|
+
"version", help="version to tag the Docker image with"
|
|
178
|
+
)
|
|
179
|
+
package_parser.add_argument("plugin-directory")
|
|
180
|
+
package_parser.add_argument(
|
|
181
|
+
"--log-format",
|
|
182
|
+
type=str,
|
|
183
|
+
default="text",
|
|
184
|
+
choices=["text", "json"],
|
|
185
|
+
help="logging format",
|
|
186
|
+
)
|
|
187
|
+
package_parser.add_argument(
|
|
188
|
+
"--log-level",
|
|
189
|
+
type=str,
|
|
190
|
+
default="info",
|
|
191
|
+
choices=["trace", "debug", "info", "warn", "error"],
|
|
192
|
+
help="log level",
|
|
193
|
+
)
|
|
194
|
+
package_parser.add_argument(
|
|
195
|
+
"-D",
|
|
196
|
+
"--dist-dir",
|
|
197
|
+
type=str,
|
|
198
|
+
help="dist directory to output the built plugin. (default: <plugin_directory>/dist)",
|
|
199
|
+
)
|
|
200
|
+
package_parser.add_argument(
|
|
201
|
+
"--docs-dir",
|
|
202
|
+
type=str,
|
|
203
|
+
help="docs directory containing markdown files to copy to the dist directory. (default: <plugin_directory>/docs)",
|
|
204
|
+
)
|
|
205
|
+
package_parser.add_argument(
|
|
206
|
+
"-m",
|
|
207
|
+
"--message",
|
|
208
|
+
type=str,
|
|
209
|
+
required=True,
|
|
210
|
+
help="message that summarizes what is new or changed in this version. Use @<file> to read from file. Supports markdown.",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def _package(self, args):
|
|
214
|
+
logger = get_logger(args)
|
|
215
|
+
self._plugin.set_logger(logger)
|
|
216
|
+
|
|
217
|
+
def _is_empty(val):
|
|
218
|
+
return val == None or len(val) == 0
|
|
219
|
+
|
|
220
|
+
if _is_empty(self._plugin.name()):
|
|
221
|
+
raise Exception("plugin name is required")
|
|
222
|
+
if _is_empty(self._plugin.team()):
|
|
223
|
+
raise Exception("plugin team is required")
|
|
224
|
+
if _is_empty(self._plugin.kind()):
|
|
225
|
+
raise Exception("plugin kind is required")
|
|
226
|
+
if _is_empty(self._plugin.dockerfile()):
|
|
227
|
+
raise Exception("plugin dockerfile is required")
|
|
228
|
+
if _is_empty(self._plugin.build_targets()):
|
|
229
|
+
raise Exception("at least one build target is required")
|
|
230
|
+
|
|
231
|
+
plugin_directory, version, message = (
|
|
232
|
+
getattr(args, "plugin-directory"),
|
|
233
|
+
getattr(args, "version"),
|
|
234
|
+
getattr(args, "message"),
|
|
235
|
+
)
|
|
236
|
+
dist_dir = (
|
|
237
|
+
"%s/dist" % plugin_directory if args.dist_dir == None else args.dist_dir
|
|
238
|
+
)
|
|
239
|
+
docs_dir = (
|
|
240
|
+
"%s/docs" % plugin_directory if args.docs_dir == None else args.docs_dir
|
|
241
|
+
)
|
|
242
|
+
Path(dist_dir).mkdir(0o755, exist_ok=True, parents=True)
|
|
243
|
+
|
|
244
|
+
self._copy_docs(logger, docs_dir, dist_dir)
|
|
245
|
+
self._write_tables_json(logger, dist_dir)
|
|
246
|
+
supported_targets = self._build_dockerfile(
|
|
247
|
+
logger, plugin_directory, dist_dir, version
|
|
248
|
+
)
|
|
249
|
+
self._write_package_json(logger, dist_dir, message, version, supported_targets)
|
|
250
|
+
logger.info("Done packaging plugin to '%s'" % dist_dir)
|
|
251
|
+
|
|
252
|
+
def _write_package_json(self, logger, dist_dir, message, version, supportedTargets):
|
|
253
|
+
package_json_path = "%s/package.json" % dist_dir
|
|
254
|
+
logger.info("Writing package.json to '%s'" % package_json_path)
|
|
255
|
+
content = {
|
|
256
|
+
"schema_version": 1,
|
|
257
|
+
"name": self._plugin.name(),
|
|
258
|
+
"team": self._plugin.team(),
|
|
259
|
+
"kind": self._plugin.kind(),
|
|
260
|
+
"version": version,
|
|
261
|
+
"message": message,
|
|
262
|
+
"protocols": [3],
|
|
263
|
+
"supported_targets": supportedTargets,
|
|
264
|
+
"package_type": "docker",
|
|
265
|
+
}
|
|
266
|
+
with open("%s/package.json" % dist_dir, "w") as f:
|
|
267
|
+
f.write(json.dumps(content, indent=2))
|
|
268
|
+
|
|
269
|
+
def _copy_docs(self, logger, docs_dir, dist_dir):
|
|
270
|
+
# check is docs_dir exists
|
|
271
|
+
if not os.path.isdir(docs_dir):
|
|
272
|
+
raise Exception("docs directory '%s' does not exist" % docs_dir)
|
|
273
|
+
|
|
274
|
+
output_docs_dir = "%s/docs" % dist_dir
|
|
275
|
+
logger.info("Copying docs from '%s' to '%s'" % (docs_dir, output_docs_dir))
|
|
276
|
+
shutil.copytree(docs_dir, output_docs_dir, dirs_exist_ok=True)
|
|
277
|
+
|
|
278
|
+
def _write_tables_json(self, logger, dist_dir):
|
|
279
|
+
if self._plugin.kind() != "source":
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
tables_json_output_path = "%s/tables.json" % dist_dir
|
|
283
|
+
logger.info("Writing tables to '%s'" % tables_json_output_path)
|
|
284
|
+
self._plugin.init(spec=b"", no_connection=True)
|
|
285
|
+
tables = self._plugin.get_tables(
|
|
286
|
+
options=plugin.plugin.TableOptions(
|
|
287
|
+
tables=["*"], skip_tables=[], skip_dependent_tables=False
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
flattened_tables = table.flatten_tables(tables)
|
|
291
|
+
|
|
292
|
+
def column_to_json(column: table.Column):
|
|
293
|
+
return {
|
|
294
|
+
"name": column.name,
|
|
295
|
+
"type": str(column.type),
|
|
296
|
+
"description": column.description,
|
|
297
|
+
"incremental_key": column.incremental_key,
|
|
298
|
+
"primary_key": column.primary_key,
|
|
299
|
+
"not_null": column.not_null,
|
|
300
|
+
"unique": column.unique,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
def table_to_json(table: table.Table):
|
|
304
|
+
return {
|
|
305
|
+
"name": table.name,
|
|
306
|
+
"title": table.title,
|
|
307
|
+
"description": table.description,
|
|
308
|
+
"is_incremental": table.is_incremental,
|
|
309
|
+
"parent": table.parent.name if table.parent else "",
|
|
310
|
+
"relations": list(map(lambda r: r.name, table.relations)),
|
|
311
|
+
"columns": list(map(column_to_json, table.columns)),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
tables_json = list(map(table_to_json, flattened_tables))
|
|
315
|
+
with open(tables_json_output_path, "w") as f:
|
|
316
|
+
f.write(json.dumps(tables_json))
|
|
317
|
+
logger.info(
|
|
318
|
+
"Wrote %d tables to '%s'" % (len(tables_json), tables_json_output_path)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def _build_dockerfile(self, logger, plugin_dir, dist_dir, version):
|
|
322
|
+
dockerfile_path = "%s/%s" % (plugin_dir, self._plugin.dockerfile())
|
|
323
|
+
if not os.path.isfile(dockerfile_path):
|
|
324
|
+
raise Exception("Dockerfile '%s' does not exist" % dockerfile_path)
|
|
325
|
+
|
|
326
|
+
def run_docker_cmd(cmd, plugin_dir):
|
|
327
|
+
result = subprocess.run(cmd, capture_output=True, cwd=plugin_dir)
|
|
328
|
+
if result.returncode != 0:
|
|
329
|
+
err = (
|
|
330
|
+
""
|
|
331
|
+
if result.stderr is None
|
|
332
|
+
else result.stderr.decode("utf-8").strip()
|
|
333
|
+
)
|
|
334
|
+
raise ChildProcessError("Unable to run Docker command: %s" % err)
|
|
335
|
+
|
|
336
|
+
def build_target(target: plugin.plugin.BuildTarget):
|
|
337
|
+
image_repository = "docker.cloudquery.io/%s/%s-%s" % (
|
|
338
|
+
self._plugin.team(),
|
|
339
|
+
self._plugin.kind(),
|
|
340
|
+
self._plugin.name(),
|
|
341
|
+
)
|
|
342
|
+
image_tag = "%s:%s-%s-%s" % (
|
|
343
|
+
image_repository,
|
|
344
|
+
version,
|
|
345
|
+
target.os,
|
|
346
|
+
target.arch,
|
|
347
|
+
)
|
|
348
|
+
image_tar = "plugin-%s-%s-%s-%s.tar" % (
|
|
349
|
+
self._plugin.name(),
|
|
350
|
+
version,
|
|
351
|
+
target.os,
|
|
352
|
+
target.arch,
|
|
353
|
+
)
|
|
354
|
+
image_path = "%s/%s" % (dist_dir, image_tar)
|
|
355
|
+
logger.info("Building docker image %s" % image_tag)
|
|
356
|
+
docker_build_arguments = [
|
|
357
|
+
"docker",
|
|
358
|
+
"buildx",
|
|
359
|
+
"build",
|
|
360
|
+
"-t",
|
|
361
|
+
image_tag,
|
|
362
|
+
"--platform",
|
|
363
|
+
"%s/%s" % (target.os, target.arch),
|
|
364
|
+
"-f",
|
|
365
|
+
dockerfile_path,
|
|
366
|
+
".",
|
|
367
|
+
"--progress",
|
|
368
|
+
"plain",
|
|
369
|
+
"--load",
|
|
370
|
+
]
|
|
371
|
+
logger.debug(
|
|
372
|
+
"Running command 'docker %s'" % " ".join(docker_build_arguments)
|
|
373
|
+
)
|
|
374
|
+
run_docker_cmd(docker_build_arguments, plugin_dir)
|
|
375
|
+
logger.debug("Saving docker image '%s' to '%s'" % (image_tag, image_path))
|
|
376
|
+
docker_save_arguments = ["docker", "save", "-o", image_path, image_tag]
|
|
377
|
+
logger.debug("Running command 'docker %s'", " ".join(docker_save_arguments))
|
|
378
|
+
run_docker_cmd(docker_save_arguments, plugin_dir)
|
|
379
|
+
return {
|
|
380
|
+
"os": target.os,
|
|
381
|
+
"arch": target.arch,
|
|
382
|
+
"path": image_tar,
|
|
383
|
+
"checksum": calc_sha256_checksum(image_path),
|
|
384
|
+
"docker_image_tag": image_tag,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
logger.info("Building %d targets" % len(self._plugin.build_targets()))
|
|
388
|
+
supported_targets = list(map(build_target, self._plugin.build_targets()))
|
|
389
|
+
return supported_targets
|
|
390
|
+
|
|
391
|
+
def _serve(self, args):
|
|
392
|
+
logger = get_logger(args)
|
|
393
|
+
self._plugin.set_logger(logger)
|
|
394
|
+
self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
|
395
|
+
discovery_pb2_grpc.add_DiscoveryServicer_to_server(
|
|
396
|
+
DiscoveryServicer([3]), self._server
|
|
397
|
+
)
|
|
398
|
+
plugin_pb2_grpc.add_PluginServicer_to_server(
|
|
399
|
+
PluginServicer(self._plugin, logger), self._server
|
|
400
|
+
)
|
|
401
|
+
self._server.add_insecure_port(args.address)
|
|
402
|
+
logger.info("Starting server", address=args.address)
|
|
403
|
+
self._server.start()
|
|
404
|
+
self._server.wait_for_termination()
|
|
405
|
+
|
|
406
|
+
def stop(self):
|
|
407
|
+
self._server.stop(5)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .keys import state_key
|
|
@@ -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[Union[str, int]] = 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(str(client_id))}")
|
|
42
|
+
for name in sorted(partition):
|
|
43
|
+
parts.append(f"{_escape(name)}{_ASSIGN}{_escape(str(partition[name]))}")
|
|
44
|
+
return _SEPARATOR.join(parts)
|