iii-helpers 0.19.4a2__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.
- iii_helpers-0.19.4a2/.gitignore +92 -0
- iii_helpers-0.19.4a2/PKG-INFO +31 -0
- iii_helpers-0.19.4a2/README.md +5 -0
- iii_helpers-0.19.4a2/pyproject.toml +57 -0
- iii_helpers-0.19.4a2/src/iii_helpers/__init__.py +1 -0
- iii_helpers-0.19.4a2/src/iii_helpers/http/__init__.py +142 -0
- iii_helpers-0.19.4a2/src/iii_helpers/py.typed +0 -0
- iii_helpers-0.19.4a2/src/iii_helpers/queue/__init__.py +18 -0
- iii_helpers-0.19.4a2/src/iii_helpers/stream/__init__.py +334 -0
- iii_helpers-0.19.4a2/src/iii_helpers/worker_connection_manager/__init__.py +183 -0
- iii_helpers-0.19.4a2/uv.lock +791 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
pytest_cache/
|
|
49
|
+
|
|
50
|
+
# Translations
|
|
51
|
+
*.mo
|
|
52
|
+
*.pot
|
|
53
|
+
|
|
54
|
+
# Environments
|
|
55
|
+
.env
|
|
56
|
+
.venv
|
|
57
|
+
env/
|
|
58
|
+
venv/
|
|
59
|
+
ENV/
|
|
60
|
+
env.bak/
|
|
61
|
+
venv.bak/
|
|
62
|
+
|
|
63
|
+
# Spyder project settings
|
|
64
|
+
.spyderproject
|
|
65
|
+
.spyproject
|
|
66
|
+
|
|
67
|
+
# Rope project settings
|
|
68
|
+
.ropeproject
|
|
69
|
+
|
|
70
|
+
# mkdocs documentation
|
|
71
|
+
/site
|
|
72
|
+
|
|
73
|
+
# mypy
|
|
74
|
+
.mypy_cache/
|
|
75
|
+
.dmypy.json
|
|
76
|
+
dmypy.json
|
|
77
|
+
|
|
78
|
+
# Pyre type checker
|
|
79
|
+
.pyre/
|
|
80
|
+
|
|
81
|
+
# pytype static type analyzer
|
|
82
|
+
.pytype/
|
|
83
|
+
|
|
84
|
+
# Cython debug symbols
|
|
85
|
+
cython_debug/
|
|
86
|
+
|
|
87
|
+
# IDE
|
|
88
|
+
.idea/
|
|
89
|
+
.vscode/
|
|
90
|
+
*.swp
|
|
91
|
+
*.swo
|
|
92
|
+
*~
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iii-helpers
|
|
3
|
+
Version: 0.19.4a2
|
|
4
|
+
Summary: Shared helper primitives across iii SDKs.
|
|
5
|
+
Project-URL: Homepage, https://github.com/iii-hq/iii
|
|
6
|
+
Project-URL: Repository, https://github.com/iii-hq/iii
|
|
7
|
+
Author: III
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: helpers,iii
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: pydantic>=2.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.2; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# iii-helpers
|
|
28
|
+
|
|
29
|
+
Shared helper primitives across the iii SDKs.
|
|
30
|
+
|
|
31
|
+
See https://github.com/iii-hq/iii for the full project.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "iii-helpers"
|
|
7
|
+
version = "0.19.4a2"
|
|
8
|
+
description = "Shared helper primitives across iii SDKs."
|
|
9
|
+
authors = [{ name = "III" }]
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["iii", "helpers"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"pydantic>=2.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/iii-hq/iii"
|
|
28
|
+
Repository = "https://github.com/iii-hq/iii"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.0",
|
|
33
|
+
"pytest-asyncio>=0.23",
|
|
34
|
+
"pytest-cov>=6.0",
|
|
35
|
+
"pytest-httpx>=0.30",
|
|
36
|
+
"mypy>=1.8",
|
|
37
|
+
"ruff>=0.2",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/iii_helpers"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 120
|
|
45
|
+
target-version = "py310"
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "W"]
|
|
49
|
+
|
|
50
|
+
[tool.mypy]
|
|
51
|
+
python_version = "3.10"
|
|
52
|
+
strict = true
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
addopts = "--cov=src/iii_helpers --cov-branch --cov-report=term-missing"
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""iii helpers."""
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""iii http helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Generic, Literal, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from iii.types import StreamRequest, StreamResponse
|
|
11
|
+
|
|
12
|
+
TInput = TypeVar("TInput")
|
|
13
|
+
TOutput = TypeVar("TOutput")
|
|
14
|
+
|
|
15
|
+
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
|
|
16
|
+
"""HTTP method accepted by :data:`HttpInvocationConfig`. Distinct from the core
|
|
17
|
+
``builtin_triggers`` HTTP method enum, which also covers HEAD/OPTIONS."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HttpAuthHmac(BaseModel):
|
|
21
|
+
"""HMAC signature verification using a shared secret."""
|
|
22
|
+
|
|
23
|
+
type: Literal["hmac"] = "hmac"
|
|
24
|
+
secret_key: str = Field(description="Environment variable name containing the HMAC shared secret.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HttpAuthBearer(BaseModel):
|
|
28
|
+
"""Bearer token authentication."""
|
|
29
|
+
|
|
30
|
+
type: Literal["bearer"] = "bearer"
|
|
31
|
+
token_key: str = Field(description="Environment variable name containing the bearer token.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HttpAuthApiKey(BaseModel):
|
|
35
|
+
"""API key sent via a custom header."""
|
|
36
|
+
|
|
37
|
+
type: Literal["api_key"] = "api_key"
|
|
38
|
+
header: str = Field(description="HTTP header name for the API key.")
|
|
39
|
+
value_key: str = Field(description="Environment variable name containing the API key value.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
HttpAuthConfig = HttpAuthHmac | HttpAuthBearer | HttpAuthApiKey
|
|
43
|
+
"""Authentication configuration for HTTP-invoked functions."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HttpInvocationConfig(BaseModel):
|
|
47
|
+
"""Config for HTTP external function invocation.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
url: Target URL for the HTTP invocation.
|
|
51
|
+
method: HTTP method. Defaults to ``'POST'``.
|
|
52
|
+
timeout_ms: Request timeout in milliseconds.
|
|
53
|
+
headers: Additional HTTP headers to include in the request.
|
|
54
|
+
auth: Authentication configuration (bearer, HMAC, or API key).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
url: str = Field(description="Target URL for the HTTP invocation.")
|
|
58
|
+
method: HttpMethod = Field(default="POST", description="HTTP method. Defaults to ``'POST'``.")
|
|
59
|
+
timeout_ms: int | None = Field(default=None, description="Request timeout in milliseconds.")
|
|
60
|
+
headers: dict[str, str] | None = Field(
|
|
61
|
+
default=None,
|
|
62
|
+
description="Additional HTTP headers to include in the request.",
|
|
63
|
+
)
|
|
64
|
+
auth: HttpAuthConfig | None = Field(
|
|
65
|
+
default=None,
|
|
66
|
+
description="Authentication configuration (bearer, HMAC, or API key).",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class HttpRequest(BaseModel, Generic[TInput]):
|
|
71
|
+
"""Represents a buffered HTTP request."""
|
|
72
|
+
|
|
73
|
+
path_params: dict[str, str] = Field(default_factory=dict)
|
|
74
|
+
query_params: dict[str, str | list[str]] = Field(default_factory=dict)
|
|
75
|
+
body: Any | None = None
|
|
76
|
+
headers: dict[str, str | list[str]] = Field(default_factory=dict)
|
|
77
|
+
method: str = "GET"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class HttpResponse(BaseModel, Generic[TOutput]):
|
|
81
|
+
"""Represents a buffered HTTP response."""
|
|
82
|
+
|
|
83
|
+
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
|
|
84
|
+
|
|
85
|
+
status_code: int = Field(alias="statusCode")
|
|
86
|
+
body: Any | None = None
|
|
87
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def http(
|
|
91
|
+
callback: Callable[[StreamRequest, StreamResponse], Awaitable[HttpResponse[Any] | None]],
|
|
92
|
+
) -> Callable[[Any], Awaitable[HttpResponse[Any] | None]]:
|
|
93
|
+
"""Wrap a streaming handler so it receives typed StreamRequest and StreamResponse.
|
|
94
|
+
|
|
95
|
+
Takes a callback ``(req, res) -> HttpResponse | None`` and returns a
|
|
96
|
+
function the iii engine can invoke directly. The wrapper converts the
|
|
97
|
+
raw dict (or ``InternalHttpRequest``) delivered by the engine into the
|
|
98
|
+
typed ``StreamRequest`` / ``StreamResponse`` pair that the callback expects.
|
|
99
|
+
"""
|
|
100
|
+
from iii.types import InternalHttpRequest, StreamRequest, StreamResponse
|
|
101
|
+
|
|
102
|
+
async def wrapper(req: Any) -> HttpResponse[Any] | None:
|
|
103
|
+
if isinstance(req, InternalHttpRequest):
|
|
104
|
+
internal = req
|
|
105
|
+
elif isinstance(req, dict):
|
|
106
|
+
internal = InternalHttpRequest(
|
|
107
|
+
path_params=req.get("path_params", {}),
|
|
108
|
+
query_params=req.get("query_params", {}),
|
|
109
|
+
body=req.get("body"),
|
|
110
|
+
headers=req.get("headers", {}),
|
|
111
|
+
method=req.get("method", "GET"),
|
|
112
|
+
response=req["response"],
|
|
113
|
+
request_body=req["request_body"],
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
internal = req
|
|
117
|
+
|
|
118
|
+
http_response = StreamResponse(internal.response)
|
|
119
|
+
http_request = StreamRequest(
|
|
120
|
+
path_params=internal.path_params,
|
|
121
|
+
query_params=internal.query_params,
|
|
122
|
+
body=internal.body,
|
|
123
|
+
headers=internal.headers,
|
|
124
|
+
method=internal.method,
|
|
125
|
+
request_body=internal.request_body,
|
|
126
|
+
)
|
|
127
|
+
return await callback(http_request, http_response)
|
|
128
|
+
|
|
129
|
+
return wrapper
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = [
|
|
133
|
+
"HttpAuthApiKey",
|
|
134
|
+
"HttpAuthBearer",
|
|
135
|
+
"HttpAuthConfig",
|
|
136
|
+
"HttpAuthHmac",
|
|
137
|
+
"HttpInvocationConfig",
|
|
138
|
+
"HttpMethod",
|
|
139
|
+
"HttpRequest",
|
|
140
|
+
"HttpResponse",
|
|
141
|
+
"http",
|
|
142
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""iii queue helpers."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EnqueueResult(BaseModel):
|
|
7
|
+
"""Result returned when a function is invoked with ``TriggerAction.Enqueue``.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
messageReceiptId: UUID assigned by the engine to the enqueued job.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
messageReceiptId: str = Field(description="UUID assigned by the engine to the enqueued job.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"EnqueueResult",
|
|
18
|
+
]
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""iii stream helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Generic, Literal, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, model_serializer
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"MergePath",
|
|
11
|
+
"StreamAuthInput",
|
|
12
|
+
"StreamAuthResult",
|
|
13
|
+
"StreamChangeEvent",
|
|
14
|
+
"StreamChangeEventDetail",
|
|
15
|
+
"StreamContext",
|
|
16
|
+
"StreamDeleteInput",
|
|
17
|
+
"StreamDeleteResult",
|
|
18
|
+
"StreamGetInput",
|
|
19
|
+
"StreamJoinLeaveEvent",
|
|
20
|
+
"StreamJoinLeaveTriggerConfig",
|
|
21
|
+
"StreamJoinResult",
|
|
22
|
+
"StreamListGroupsInput",
|
|
23
|
+
"StreamListInput",
|
|
24
|
+
"StreamSetInput",
|
|
25
|
+
"StreamSetResult",
|
|
26
|
+
"StreamTriggerConfig",
|
|
27
|
+
"StreamUpdateInput",
|
|
28
|
+
"StreamUpdateResult",
|
|
29
|
+
"UpdateAppend",
|
|
30
|
+
"UpdateDecrement",
|
|
31
|
+
"UpdateIncrement",
|
|
32
|
+
"UpdateMerge",
|
|
33
|
+
"UpdateOp",
|
|
34
|
+
"UpdateOpError",
|
|
35
|
+
"UpdateRemove",
|
|
36
|
+
"UpdateSet",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
TData = TypeVar("TData")
|
|
40
|
+
|
|
41
|
+
# Path target for a ``merge`` / ``append`` op. Accepts either a single
|
|
42
|
+
# string (legacy / first-level key) or a list of literal segments
|
|
43
|
+
# (nested path). Mirrors the Node ``string | string[]`` alias and the
|
|
44
|
+
# Rust ``MergePath`` enum.
|
|
45
|
+
MergePath = str | list[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class StreamAuthInput(BaseModel):
|
|
49
|
+
"""Input for stream authentication."""
|
|
50
|
+
|
|
51
|
+
headers: dict[str, str]
|
|
52
|
+
path: str
|
|
53
|
+
query_params: dict[str, list[str]]
|
|
54
|
+
addr: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StreamAuthResult(BaseModel):
|
|
58
|
+
"""Result of stream authentication."""
|
|
59
|
+
|
|
60
|
+
context: Any | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
StreamContext = Any
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class StreamJoinLeaveEvent(BaseModel):
|
|
67
|
+
"""Event for stream join/leave."""
|
|
68
|
+
|
|
69
|
+
subscription_id: str
|
|
70
|
+
stream_name: str
|
|
71
|
+
group_id: str
|
|
72
|
+
id: str | None = None
|
|
73
|
+
context: Any | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class StreamJoinResult(BaseModel):
|
|
77
|
+
"""Result of stream join."""
|
|
78
|
+
|
|
79
|
+
unauthorized: bool
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class StreamGetInput(BaseModel):
|
|
83
|
+
"""Input for stream get operation."""
|
|
84
|
+
|
|
85
|
+
stream_name: str
|
|
86
|
+
group_id: str
|
|
87
|
+
item_id: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class StreamSetInput(BaseModel):
|
|
91
|
+
"""Input for stream set operation."""
|
|
92
|
+
|
|
93
|
+
stream_name: str
|
|
94
|
+
group_id: str
|
|
95
|
+
item_id: str
|
|
96
|
+
data: Any
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class StreamDeleteInput(BaseModel):
|
|
100
|
+
"""Input for stream delete operation."""
|
|
101
|
+
|
|
102
|
+
stream_name: str
|
|
103
|
+
group_id: str
|
|
104
|
+
item_id: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class StreamListInput(BaseModel):
|
|
108
|
+
"""Input for stream list operation."""
|
|
109
|
+
|
|
110
|
+
stream_name: str
|
|
111
|
+
group_id: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class StreamListGroupsInput(BaseModel):
|
|
115
|
+
"""Input for stream list groups operation."""
|
|
116
|
+
|
|
117
|
+
stream_name: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class StreamUpdateInput(BaseModel):
|
|
121
|
+
"""Input for stream update operation."""
|
|
122
|
+
|
|
123
|
+
stream_name: str
|
|
124
|
+
group_id: str
|
|
125
|
+
item_id: str
|
|
126
|
+
ops: list["UpdateOp"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class UpdateOpError(BaseModel):
|
|
130
|
+
"""Per-op error returned by ``state::update`` / ``stream::update``.
|
|
131
|
+
|
|
132
|
+
Currently emitted only by the ``merge`` op when input violates the
|
|
133
|
+
new validation bounds. Successfully applied ops are still
|
|
134
|
+
reflected in the response's ``new_value``.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
op_index: int
|
|
138
|
+
code: str
|
|
139
|
+
message: str
|
|
140
|
+
doc_url: str | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class StreamSetResult(BaseModel, Generic[TData]):
|
|
144
|
+
"""Result of stream set operation."""
|
|
145
|
+
|
|
146
|
+
old_value: TData | None = None
|
|
147
|
+
new_value: TData
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class StreamUpdateResult(BaseModel, Generic[TData]):
|
|
151
|
+
"""Result of stream update operation."""
|
|
152
|
+
|
|
153
|
+
old_value: TData | None = None
|
|
154
|
+
new_value: TData
|
|
155
|
+
# Per-op errors. Emitted by ``merge`` and ``append`` for validation
|
|
156
|
+
# rejections (path/value bounds, proto-pollution segments) and by
|
|
157
|
+
# ``append`` for the ``append.type_mismatch`` and
|
|
158
|
+
# ``append.target_not_object`` surfaces. Field is omitted from the
|
|
159
|
+
# JSON wire when empty. ``default_factory`` is used (not ``= []``)
|
|
160
|
+
# to keep Pydantic's parameterized-Generic + default handling
|
|
161
|
+
# well-behaved across Python versions.
|
|
162
|
+
errors: list[UpdateOpError] = Field(default_factory=list)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class StreamDeleteResult(BaseModel):
|
|
166
|
+
"""Result of stream delete operation."""
|
|
167
|
+
|
|
168
|
+
old_value: Any | None = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class UpdateSet(BaseModel):
|
|
172
|
+
"""Set operation for stream update."""
|
|
173
|
+
|
|
174
|
+
type: str = "set"
|
|
175
|
+
path: str
|
|
176
|
+
value: Any
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class UpdateIncrement(BaseModel):
|
|
180
|
+
"""Increment operation for stream update."""
|
|
181
|
+
|
|
182
|
+
type: str = "increment"
|
|
183
|
+
path: str
|
|
184
|
+
by: int | float
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class UpdateDecrement(BaseModel):
|
|
188
|
+
"""Decrement operation for stream update."""
|
|
189
|
+
|
|
190
|
+
type: str = "decrement"
|
|
191
|
+
path: str
|
|
192
|
+
by: int | float
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class UpdateAppend(BaseModel):
|
|
196
|
+
"""Append an element to an array, concatenate a string, or push at a nested path.
|
|
197
|
+
|
|
198
|
+
The target is the root (when ``path`` is omitted, an empty string,
|
|
199
|
+
or an empty list), a single first-level key (when ``path`` is a
|
|
200
|
+
non-empty string), or an arbitrary nested location (when ``path``
|
|
201
|
+
is a list of literal segments).
|
|
202
|
+
|
|
203
|
+
Path forms accepted (mirrors :class:`UpdateMerge` after #1547):
|
|
204
|
+
- ``None`` / ``""`` / ``[]``: append at the root.
|
|
205
|
+
- ``"foo"``: append at the first-level key ``foo``. A dotted
|
|
206
|
+
string like ``"a.b"`` is the literal key ``"a.b"``, *not*
|
|
207
|
+
traversed as ``a -> b``.
|
|
208
|
+
- ``["a", "b", "c"]``: nested path; each element is a literal
|
|
209
|
+
segment.
|
|
210
|
+
|
|
211
|
+
Engine semantics:
|
|
212
|
+
- Missing/non-object intermediates along a nested path are
|
|
213
|
+
auto-created/replaced with ``{}``.
|
|
214
|
+
- At the leaf:
|
|
215
|
+
- missing/null + nested path -> ``[value]`` (always an array)
|
|
216
|
+
- missing/null + single-string path -> string-as-string for
|
|
217
|
+
the string-concat tier, otherwise ``[value]``
|
|
218
|
+
- existing array -> push
|
|
219
|
+
- existing string + string value -> concatenate
|
|
220
|
+
- existing object/scalar at the leaf -> ``append.type_mismatch``
|
|
221
|
+
|
|
222
|
+
Validation: invalid paths (depth > 32 segments, segment > 256
|
|
223
|
+
bytes, or any ``__proto__`` / ``constructor`` / ``prototype``
|
|
224
|
+
segment) are rejected with a structured error in the ``errors``
|
|
225
|
+
field of the ``state::update`` / ``stream::update`` response. The
|
|
226
|
+
append does not apply when an error is returned for that op.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
type: str = "append"
|
|
230
|
+
# Optional. Accepts a single string (legacy / first-level key) or
|
|
231
|
+
# a list of literal segments (nested append). ``None`` / ``""`` /
|
|
232
|
+
# ``[]`` all route to root append. See :data:`MergePath`.
|
|
233
|
+
path: MergePath | None = None
|
|
234
|
+
value: Any
|
|
235
|
+
|
|
236
|
+
@model_serializer(mode="wrap")
|
|
237
|
+
def _omit_none_path(self, handler): # type: ignore[no-untyped-def]
|
|
238
|
+
# Drop ``path: None`` from the wire so cross-SDK consumers see
|
|
239
|
+
# the field absent rather than ``null``. Mirrors the Rust
|
|
240
|
+
# ``#[serde(skip_serializing_if = "Option::is_none")]`` on
|
|
241
|
+
# ``UpdateOp::Append.path``.
|
|
242
|
+
data = handler(self)
|
|
243
|
+
if data.get("path") is None:
|
|
244
|
+
data.pop("path", None)
|
|
245
|
+
return data
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class UpdateRemove(BaseModel):
|
|
249
|
+
"""Remove operation for stream update."""
|
|
250
|
+
|
|
251
|
+
type: str = "remove"
|
|
252
|
+
path: str
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class UpdateMerge(BaseModel):
|
|
256
|
+
"""Shallow merge an object into the target.
|
|
257
|
+
|
|
258
|
+
The target is the root (when ``path`` is omitted, an empty string,
|
|
259
|
+
or an empty list) or an arbitrary nested location specified by an
|
|
260
|
+
array of literal segments.
|
|
261
|
+
|
|
262
|
+
Path forms accepted:
|
|
263
|
+
- ``None`` / ``""`` / ``[]``: merge at the root.
|
|
264
|
+
- ``"foo"``: equivalent to ``["foo"]`` -- single first-level key.
|
|
265
|
+
- ``["a", "b", "c"]``: nested path. Each element is a *literal*
|
|
266
|
+
key. ``["a.b"]`` writes a single key named ``"a.b"``, not
|
|
267
|
+
``a -> b``.
|
|
268
|
+
|
|
269
|
+
Engine semantics:
|
|
270
|
+
- Missing or non-object intermediates along the path are
|
|
271
|
+
auto-replaced with ``{}``.
|
|
272
|
+
- The merge is shallow at the target node (top-level keys of
|
|
273
|
+
``value`` overwrite same-named keys; siblings preserved).
|
|
274
|
+
|
|
275
|
+
Validation: invalid paths/values (depth > 32 segments, segment >
|
|
276
|
+
256 bytes, value depth > 16, > 1024 top-level keys, or any
|
|
277
|
+
``__proto__`` / ``constructor`` / ``prototype`` segment or
|
|
278
|
+
top-level key) are rejected with a structured error in the
|
|
279
|
+
``errors`` array of the ``state::update`` / ``stream::update``
|
|
280
|
+
response. The merge does not apply when an error is returned.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
type: str = "merge"
|
|
284
|
+
# Optional. Accepts a single string or a list of literal segments.
|
|
285
|
+
# Pydantic resolves ``str | list[str]`` via smart-union: string
|
|
286
|
+
# input -> str, array input -> list[str]. See :data:`MergePath`.
|
|
287
|
+
path: MergePath | None = None
|
|
288
|
+
value: Any
|
|
289
|
+
|
|
290
|
+
@model_serializer(mode="wrap")
|
|
291
|
+
def _omit_none_path(self, handler): # type: ignore[no-untyped-def]
|
|
292
|
+
# Mirrors the same skip-when-none rule applied to
|
|
293
|
+
# ``UpdateOp::Merge.path`` in the Rust SDK so cross-SDK wire
|
|
294
|
+
# payloads are byte-identical for root merges.
|
|
295
|
+
data = handler(self)
|
|
296
|
+
if data.get("path") is None:
|
|
297
|
+
data.pop("path", None)
|
|
298
|
+
return data
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
UpdateOp = UpdateSet | UpdateIncrement | UpdateDecrement | UpdateAppend | UpdateRemove | UpdateMerge
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class StreamTriggerConfig(BaseModel):
|
|
305
|
+
"""Trigger config for ``stream`` triggers. Filters which item changes fire the handler."""
|
|
306
|
+
|
|
307
|
+
stream_name: str
|
|
308
|
+
group_id: str | None = None
|
|
309
|
+
item_id: str | None = None
|
|
310
|
+
condition_function_id: str | None = None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class StreamJoinLeaveTriggerConfig(BaseModel):
|
|
314
|
+
"""Trigger config for ``stream:join`` and ``stream:leave`` triggers."""
|
|
315
|
+
|
|
316
|
+
condition_function_id: str | None = None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class StreamChangeEventDetail(BaseModel):
|
|
320
|
+
"""Detail of a stream change event containing the mutation type and data."""
|
|
321
|
+
|
|
322
|
+
type: Literal["create", "update", "delete"]
|
|
323
|
+
data: Any
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class StreamChangeEvent(BaseModel):
|
|
327
|
+
"""Handler input for ``stream`` triggers, fired when an item changes."""
|
|
328
|
+
|
|
329
|
+
type: Literal["stream"]
|
|
330
|
+
timestamp: int
|
|
331
|
+
streamName: str
|
|
332
|
+
groupId: str
|
|
333
|
+
id: str | None = None
|
|
334
|
+
event: StreamChangeEventDetail
|