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.
@@ -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.
@@ -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,3 @@
1
+ from serto._harness import main
2
+
3
+ main()
@@ -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
+ serto
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()