serto-sdk 1.5.0__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.
- serto_sdk-1.5.0/LICENSE +21 -0
- serto_sdk-1.5.0/PKG-INFO +80 -0
- serto_sdk-1.5.0/README.md +62 -0
- serto_sdk-1.5.0/pyproject.toml +29 -0
- serto_sdk-1.5.0/serto/__init__.py +17 -0
- serto_sdk-1.5.0/serto/__main__.py +3 -0
- serto_sdk-1.5.0/serto/_harness.py +86 -0
- serto_sdk-1.5.0/serto/context.py +73 -0
- serto_sdk-1.5.0/serto_sdk.egg-info/PKG-INFO +80 -0
- serto_sdk-1.5.0/serto_sdk.egg-info/SOURCES.txt +12 -0
- serto_sdk-1.5.0/serto_sdk.egg-info/dependency_links.txt +1 -0
- serto_sdk-1.5.0/serto_sdk.egg-info/top_level.txt +1 -0
- serto_sdk-1.5.0/setup.cfg +4 -0
- serto_sdk-1.5.0/tests/test_harness.py +70 -0
serto_sdk-1.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Craytech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
serto_sdk-1.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: serto-sdk
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Serto Python SDK — write integrations in Python that run on the Serto platform
|
|
5
|
+
Author: Craytech
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/neweyc/integration-platform
|
|
8
|
+
Project-URL: Repository, https://github.com/neweyc/integration-platform
|
|
9
|
+
Keywords: serto,integration,automation,workflow
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Serto Python SDK
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install serto-sdk # distribution name; the import package is `serto`
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Write Serto integrations in Python. An integration is any callable taking a `Context`:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# main.py
|
|
29
|
+
def handler(ctx):
|
|
30
|
+
ctx.logger.info(f"Running in {ctx.execution.environment}")
|
|
31
|
+
|
|
32
|
+
order = ctx.payload_json() # webhook/message body, parsed
|
|
33
|
+
api_key = ctx.secrets["API_KEY"] # provisioned secret
|
|
34
|
+
|
|
35
|
+
# ... do the work ...
|
|
36
|
+
|
|
37
|
+
ctx.publish("orders.synced", {"count": 42}) # message other integrations can subscribe to
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Declare it in a `serto.json` next to your code:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"manifestVersion": "1",
|
|
45
|
+
"runtime": "python",
|
|
46
|
+
"integrations": [
|
|
47
|
+
{
|
|
48
|
+
"name": "Sync Orders",
|
|
49
|
+
"slug": "sync-orders",
|
|
50
|
+
"entrypoint": "main.py:handler",
|
|
51
|
+
"triggers": [{ "type": "scheduled", "cron": "0 * * * *" }],
|
|
52
|
+
"requiredSecrets": ["API_KEY"]
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## How it runs
|
|
59
|
+
|
|
60
|
+
The Serto agent launches `python -m serto` in your integration's directory, sends the invocation on
|
|
61
|
+
stdin, and reads structured events (logs, published messages, the result) from stdout. The protocol is
|
|
62
|
+
documented in [`docs/multi-language-runtimes.md`](../../docs/multi-language-runtimes.md). `async def`
|
|
63
|
+
handlers are supported.
|
|
64
|
+
|
|
65
|
+
## The Context
|
|
66
|
+
|
|
67
|
+
| member | description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `ctx.logger` | `.trace/.debug/.info/.warning/.error/.critical` — captured into execution history |
|
|
70
|
+
| `ctx.secrets` | dict of secrets provisioned for the environment |
|
|
71
|
+
| `ctx.payload` / `ctx.payload_json()` | raw / parsed webhook or message body (`None` for scheduled) |
|
|
72
|
+
| `ctx.trigger` | how the run was triggered (`ctx.trigger["type"]` + source-specific fields) |
|
|
73
|
+
| `ctx.execution` | `.execution_id`, `.integration_name`, `.environment`, `.scheduled_at`, … |
|
|
74
|
+
| `ctx.publish(subject, body)` | publish a message; a non-string body is JSON-encoded |
|
|
75
|
+
|
|
76
|
+
## Tests
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
cd sdks/python && python -m unittest discover -s tests
|
|
80
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Serto Python SDK
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
pip install serto-sdk # distribution name; the import package is `serto`
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Write Serto integrations in Python. An integration is any callable taking a `Context`:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# main.py
|
|
11
|
+
def handler(ctx):
|
|
12
|
+
ctx.logger.info(f"Running in {ctx.execution.environment}")
|
|
13
|
+
|
|
14
|
+
order = ctx.payload_json() # webhook/message body, parsed
|
|
15
|
+
api_key = ctx.secrets["API_KEY"] # provisioned secret
|
|
16
|
+
|
|
17
|
+
# ... do the work ...
|
|
18
|
+
|
|
19
|
+
ctx.publish("orders.synced", {"count": 42}) # message other integrations can subscribe to
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Declare it in a `serto.json` next to your code:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"manifestVersion": "1",
|
|
27
|
+
"runtime": "python",
|
|
28
|
+
"integrations": [
|
|
29
|
+
{
|
|
30
|
+
"name": "Sync Orders",
|
|
31
|
+
"slug": "sync-orders",
|
|
32
|
+
"entrypoint": "main.py:handler",
|
|
33
|
+
"triggers": [{ "type": "scheduled", "cron": "0 * * * *" }],
|
|
34
|
+
"requiredSecrets": ["API_KEY"]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How it runs
|
|
41
|
+
|
|
42
|
+
The Serto agent launches `python -m serto` in your integration's directory, sends the invocation on
|
|
43
|
+
stdin, and reads structured events (logs, published messages, the result) from stdout. The protocol is
|
|
44
|
+
documented in [`docs/multi-language-runtimes.md`](../../docs/multi-language-runtimes.md). `async def`
|
|
45
|
+
handlers are supported.
|
|
46
|
+
|
|
47
|
+
## The Context
|
|
48
|
+
|
|
49
|
+
| member | description |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `ctx.logger` | `.trace/.debug/.info/.warning/.error/.critical` — captured into execution history |
|
|
52
|
+
| `ctx.secrets` | dict of secrets provisioned for the environment |
|
|
53
|
+
| `ctx.payload` / `ctx.payload_json()` | raw / parsed webhook or message body (`None` for scheduled) |
|
|
54
|
+
| `ctx.trigger` | how the run was triggered (`ctx.trigger["type"]` + source-specific fields) |
|
|
55
|
+
| `ctx.execution` | `.execution_id`, `.integration_name`, `.environment`, `.scheduled_at`, … |
|
|
56
|
+
| `ctx.publish(subject, body)` | publish a message; a non-string body is JSON-encoded |
|
|
57
|
+
|
|
58
|
+
## Tests
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
cd sdks/python && python -m unittest discover -s tests
|
|
62
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# Distribution name on PyPI is "serto-sdk" (the bare "serto" name is already taken); the import package
|
|
7
|
+
# is still `serto` (import serto). The release workflow overrides the version from the git tag.
|
|
8
|
+
name = "serto-sdk"
|
|
9
|
+
version = "1.5.0"
|
|
10
|
+
description = "Serto Python SDK — write integrations in Python that run on the Serto platform"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
authors = [{ name = "Craytech" }]
|
|
15
|
+
keywords = ["serto", "integration", "automation", "workflow"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/neweyc/integration-platform"
|
|
26
|
+
Repository = "https://github.com/neweyc/integration-platform"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
include = ["serto*"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Serto Python SDK — write integrations in Python that run on the Serto platform.
|
|
2
|
+
|
|
3
|
+
A handler is any callable taking a single ``Context`` argument::
|
|
4
|
+
|
|
5
|
+
def handler(ctx):
|
|
6
|
+
ctx.logger.info("running")
|
|
7
|
+
data = ctx.payload_json()
|
|
8
|
+
ctx.publish("orders.synced", {"count": 42})
|
|
9
|
+
|
|
10
|
+
Declare it in ``serto.json`` with an entrypoint of ``your_file.py:handler``. See
|
|
11
|
+
docs/multi-language-runtimes.md.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .context import Context, Execution, Logger
|
|
15
|
+
|
|
16
|
+
__all__ = ["Context", "Execution", "Logger"]
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""The Serto Python harness — the mirror of the agent's SubprocessRunner.
|
|
2
|
+
|
|
3
|
+
The agent launches this (``python -m serto``) in the package directory, writes one invocation JSON object
|
|
4
|
+
to stdin, and reads wire-protocol events (JSON-lines) from stdout. The harness reads the invocation,
|
|
5
|
+
resolves the declared entrypoint, runs it with a Context, and emits a terminal ``result`` event.
|
|
6
|
+
|
|
7
|
+
Contract: docs/multi-language-runtimes.md.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import importlib
|
|
12
|
+
import importlib.util
|
|
13
|
+
import inspect
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import traceback
|
|
18
|
+
|
|
19
|
+
from .context import Context
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_entrypoint(spec):
|
|
23
|
+
"""Resolve an entrypoint string into a callable.
|
|
24
|
+
|
|
25
|
+
Forms:
|
|
26
|
+
* ``file.py:function`` — load the file (relative to the working directory) and read ``function``.
|
|
27
|
+
* ``module:function`` — import the module and read ``function``.
|
|
28
|
+
"""
|
|
29
|
+
if not spec or ":" not in spec:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"Entrypoint must be 'file.py:function' or 'module:function', got %r" % (spec,))
|
|
32
|
+
|
|
33
|
+
module_part, func_name = spec.rsplit(":", 1)
|
|
34
|
+
|
|
35
|
+
if module_part.endswith(".py"):
|
|
36
|
+
path = os.path.abspath(module_part)
|
|
37
|
+
if not os.path.exists(path):
|
|
38
|
+
raise FileNotFoundError("Entrypoint file not found: %s" % path)
|
|
39
|
+
mod_name = os.path.splitext(os.path.basename(path))[0]
|
|
40
|
+
loaded = importlib.util.spec_from_file_location(mod_name, path)
|
|
41
|
+
module = importlib.util.module_from_spec(loaded)
|
|
42
|
+
loaded.loader.exec_module(module)
|
|
43
|
+
else:
|
|
44
|
+
module = importlib.import_module(module_part)
|
|
45
|
+
|
|
46
|
+
if not hasattr(module, func_name):
|
|
47
|
+
raise AttributeError("Entrypoint '%s' has no attribute '%s'" % (module_part, func_name))
|
|
48
|
+
return getattr(module, func_name)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run(invocation, emit):
|
|
52
|
+
"""Resolve and execute the integration. Raises on failure; the caller maps that to a result event.
|
|
53
|
+
|
|
54
|
+
Supports both sync handlers and ``async def`` handlers.
|
|
55
|
+
"""
|
|
56
|
+
ctx = Context(invocation, emit)
|
|
57
|
+
handler = _resolve_entrypoint(invocation.get("entrypoint", ""))
|
|
58
|
+
result = handler(ctx)
|
|
59
|
+
if inspect.iscoroutine(result):
|
|
60
|
+
asyncio.run(result)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main(stdin=None, stdout=None):
|
|
64
|
+
"""Entry point for ``python -m serto``. Reads the invocation, runs it, emits a result.
|
|
65
|
+
|
|
66
|
+
stdin/stdout are injectable for testing. The integration's own stdout is redirected to stderr so a
|
|
67
|
+
stray ``print()`` can never corrupt the JSON-lines protocol channel.
|
|
68
|
+
"""
|
|
69
|
+
protocol_out = stdout if stdout is not None else sys.stdout
|
|
70
|
+
source = stdin if stdin is not None else sys.stdin
|
|
71
|
+
|
|
72
|
+
saved_stdout = sys.stdout
|
|
73
|
+
sys.stdout = sys.stderr
|
|
74
|
+
|
|
75
|
+
def emit(event):
|
|
76
|
+
protocol_out.write(json.dumps(event) + "\n")
|
|
77
|
+
protocol_out.flush()
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
invocation = json.loads(source.read())
|
|
81
|
+
run(invocation, emit)
|
|
82
|
+
emit({"type": "result", "succeeded": True, "error": None})
|
|
83
|
+
except Exception:
|
|
84
|
+
emit({"type": "result", "succeeded": False, "error": traceback.format_exc()})
|
|
85
|
+
finally:
|
|
86
|
+
sys.stdout = saved_stdout
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""The execution context handed to a Python integration.
|
|
2
|
+
|
|
3
|
+
Mirrors the .NET IIntegrationContext: secrets, a structured logger, the trigger, the payload, and a way
|
|
4
|
+
to publish messages. Logs and messages are emitted as wire-protocol events (see _harness); the agent
|
|
5
|
+
forwards them onto the same pipeline a C# integration uses, so a Python run is indistinguishable
|
|
6
|
+
downstream. See docs/multi-language-runtimes.md.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Execution:
|
|
13
|
+
"""Metadata about the current run."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, data):
|
|
16
|
+
self.execution_id = data.get("executionId")
|
|
17
|
+
self.integration_id = data.get("integrationId")
|
|
18
|
+
self.integration_name = data.get("integrationName")
|
|
19
|
+
self.environment = data.get("environment")
|
|
20
|
+
self.scheduled_at = data.get("scheduledAt")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Logger:
|
|
24
|
+
"""Structured logger. Levels match .NET LogLevel names so they render identically in execution history."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, emit):
|
|
27
|
+
self._emit = emit
|
|
28
|
+
|
|
29
|
+
def _log(self, level, message, exception=None):
|
|
30
|
+
event = {"type": "log", "level": level, "message": str(message)}
|
|
31
|
+
if exception is not None:
|
|
32
|
+
event["exception"] = str(exception)
|
|
33
|
+
self._emit(event)
|
|
34
|
+
|
|
35
|
+
def trace(self, message):
|
|
36
|
+
self._log("Trace", message)
|
|
37
|
+
|
|
38
|
+
def debug(self, message):
|
|
39
|
+
self._log("Debug", message)
|
|
40
|
+
|
|
41
|
+
def info(self, message):
|
|
42
|
+
self._log("Information", message)
|
|
43
|
+
|
|
44
|
+
def warning(self, message):
|
|
45
|
+
self._log("Warning", message)
|
|
46
|
+
|
|
47
|
+
def error(self, message, exception=None):
|
|
48
|
+
self._log("Error", message, exception)
|
|
49
|
+
|
|
50
|
+
def critical(self, message, exception=None):
|
|
51
|
+
self._log("Critical", message, exception)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Context:
|
|
55
|
+
def __init__(self, invocation, emit):
|
|
56
|
+
self._emit = emit
|
|
57
|
+
self.execution = Execution(invocation.get("execution", {}))
|
|
58
|
+
# The trigger is passed through verbatim; read trigger["type"] and source-specific fields.
|
|
59
|
+
self.trigger = invocation.get("trigger", {})
|
|
60
|
+
# Raw request/message body for webhook- and message-triggered runs; None otherwise.
|
|
61
|
+
self.payload = invocation.get("payload")
|
|
62
|
+
self.secrets = invocation.get("secrets", {})
|
|
63
|
+
self.logger = Logger(emit)
|
|
64
|
+
|
|
65
|
+
def payload_json(self):
|
|
66
|
+
"""Deserialize the payload as JSON, or None when there is no payload."""
|
|
67
|
+
return json.loads(self.payload) if self.payload else None
|
|
68
|
+
|
|
69
|
+
def publish(self, subject, body):
|
|
70
|
+
"""Publish a message other integrations can subscribe to. A non-string body is JSON-encoded."""
|
|
71
|
+
if not isinstance(body, str):
|
|
72
|
+
body = json.dumps(body)
|
|
73
|
+
self._emit({"type": "message", "subject": subject, "body": body})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: serto-sdk
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Serto Python SDK — write integrations in Python that run on the Serto platform
|
|
5
|
+
Author: Craytech
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/neweyc/integration-platform
|
|
8
|
+
Project-URL: Repository, https://github.com/neweyc/integration-platform
|
|
9
|
+
Keywords: serto,integration,automation,workflow
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Serto Python SDK
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install serto-sdk # distribution name; the import package is `serto`
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Write Serto integrations in Python. An integration is any callable taking a `Context`:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# main.py
|
|
29
|
+
def handler(ctx):
|
|
30
|
+
ctx.logger.info(f"Running in {ctx.execution.environment}")
|
|
31
|
+
|
|
32
|
+
order = ctx.payload_json() # webhook/message body, parsed
|
|
33
|
+
api_key = ctx.secrets["API_KEY"] # provisioned secret
|
|
34
|
+
|
|
35
|
+
# ... do the work ...
|
|
36
|
+
|
|
37
|
+
ctx.publish("orders.synced", {"count": 42}) # message other integrations can subscribe to
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Declare it in a `serto.json` next to your code:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"manifestVersion": "1",
|
|
45
|
+
"runtime": "python",
|
|
46
|
+
"integrations": [
|
|
47
|
+
{
|
|
48
|
+
"name": "Sync Orders",
|
|
49
|
+
"slug": "sync-orders",
|
|
50
|
+
"entrypoint": "main.py:handler",
|
|
51
|
+
"triggers": [{ "type": "scheduled", "cron": "0 * * * *" }],
|
|
52
|
+
"requiredSecrets": ["API_KEY"]
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## How it runs
|
|
59
|
+
|
|
60
|
+
The Serto agent launches `python -m serto` in your integration's directory, sends the invocation on
|
|
61
|
+
stdin, and reads structured events (logs, published messages, the result) from stdout. The protocol is
|
|
62
|
+
documented in [`docs/multi-language-runtimes.md`](../../docs/multi-language-runtimes.md). `async def`
|
|
63
|
+
handlers are supported.
|
|
64
|
+
|
|
65
|
+
## The Context
|
|
66
|
+
|
|
67
|
+
| member | description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `ctx.logger` | `.trace/.debug/.info/.warning/.error/.critical` — captured into execution history |
|
|
70
|
+
| `ctx.secrets` | dict of secrets provisioned for the environment |
|
|
71
|
+
| `ctx.payload` / `ctx.payload_json()` | raw / parsed webhook or message body (`None` for scheduled) |
|
|
72
|
+
| `ctx.trigger` | how the run was triggered (`ctx.trigger["type"]` + source-specific fields) |
|
|
73
|
+
| `ctx.execution` | `.execution_id`, `.integration_name`, `.environment`, `.scheduled_at`, … |
|
|
74
|
+
| `ctx.publish(subject, body)` | publish a message; a non-string body is JSON-encoded |
|
|
75
|
+
|
|
76
|
+
## Tests
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
cd sdks/python && python -m unittest discover -s tests
|
|
80
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
serto/__init__.py
|
|
5
|
+
serto/__main__.py
|
|
6
|
+
serto/_harness.py
|
|
7
|
+
serto/context.py
|
|
8
|
+
serto_sdk.egg-info/PKG-INFO
|
|
9
|
+
serto_sdk.egg-info/SOURCES.txt
|
|
10
|
+
serto_sdk.egg-info/dependency_links.txt
|
|
11
|
+
serto_sdk.egg-info/top_level.txt
|
|
12
|
+
tests/test_harness.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
serto
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
8
|
+
|
|
9
|
+
from serto._harness import main, run # noqa: E402
|
|
10
|
+
|
|
11
|
+
FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures.py")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def invocation(func, payload=None, secrets=None):
|
|
15
|
+
return {
|
|
16
|
+
"protocolVersion": "1",
|
|
17
|
+
"entrypoint": FIXTURES + ":" + func,
|
|
18
|
+
"execution": {"environment": "production"},
|
|
19
|
+
"trigger": {"type": "manual"},
|
|
20
|
+
"payload": payload,
|
|
21
|
+
"secrets": secrets or {},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RunTests(unittest.TestCase):
|
|
26
|
+
def _events(self, func, **kw):
|
|
27
|
+
events = []
|
|
28
|
+
run(invocation(func, **kw), events.append)
|
|
29
|
+
return events
|
|
30
|
+
|
|
31
|
+
def test_logs_and_publishes(self):
|
|
32
|
+
events = self._events("success_handler")
|
|
33
|
+
self.assertIn({"type": "log", "level": "Information", "message": "ran ok"}, events)
|
|
34
|
+
message = next(e for e in events if e["type"] == "message")
|
|
35
|
+
self.assertEqual("test.subject", message["subject"])
|
|
36
|
+
self.assertEqual({"k": 1}, json.loads(message["body"]))
|
|
37
|
+
|
|
38
|
+
def test_secrets_are_available(self):
|
|
39
|
+
events = self._events("secret_handler", secrets={"API_KEY": "xyz"})
|
|
40
|
+
self.assertTrue(any("secret=xyz" in e.get("message", "") for e in events))
|
|
41
|
+
|
|
42
|
+
def test_failure_propagates(self):
|
|
43
|
+
with self.assertRaises(RuntimeError):
|
|
44
|
+
self._events("failing_handler")
|
|
45
|
+
|
|
46
|
+
def test_async_handler_is_awaited(self):
|
|
47
|
+
events = self._events("async_handler")
|
|
48
|
+
self.assertTrue(any(e.get("message") == "async ran" for e in events))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MainTests(unittest.TestCase):
|
|
52
|
+
def _run_main(self, func, **kw):
|
|
53
|
+
out = io.StringIO()
|
|
54
|
+
main(stdin=io.StringIO(json.dumps(invocation(func, **kw))), stdout=out)
|
|
55
|
+
return [json.loads(line) for line in out.getvalue().splitlines() if line.strip()]
|
|
56
|
+
|
|
57
|
+
def test_success_emits_result_true(self):
|
|
58
|
+
events = self._run_main("success_handler")
|
|
59
|
+
self.assertEqual({"type": "result", "succeeded": True, "error": None}, events[-1])
|
|
60
|
+
|
|
61
|
+
def test_failure_emits_result_false_with_traceback(self):
|
|
62
|
+
events = self._run_main("failing_handler")
|
|
63
|
+
result = events[-1]
|
|
64
|
+
self.assertEqual("result", result["type"])
|
|
65
|
+
self.assertFalse(result["succeeded"])
|
|
66
|
+
self.assertIn("boom", result["error"])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
unittest.main()
|