lumera 0.28.0.dev2__tar.gz → 0.31.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.
- {lumera-0.28.0.dev2 → lumera-0.31.0}/PKG-INFO +8 -1
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/__init__.py +44 -1
- lumera-0.31.0/lumera/function_resources.py +110 -0
- lumera-0.31.0/lumera/functions/__init__.py +118 -0
- lumera-0.31.0/lumera/functions/__main__.py +5 -0
- lumera-0.31.0/lumera/functions/_artifacts.py +52 -0
- lumera-0.31.0/lumera/functions/_config.py +110 -0
- lumera-0.31.0/lumera/functions/_context.py +405 -0
- lumera-0.31.0/lumera/functions/_contract.py +2070 -0
- lumera-0.31.0/lumera/functions/_errors.py +170 -0
- lumera-0.31.0/lumera/functions/_execution.py +107 -0
- lumera-0.31.0/lumera/functions/_registry.py +211 -0
- lumera-0.31.0/lumera/functions/_runner.py +344 -0
- lumera-0.31.0/lumera/functions/_runtime.py +753 -0
- lumera-0.31.0/lumera/functions/testing.py +109 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/google.py +3 -1
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/integrations/__init__.py +2 -1
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/integrations/google.py +15 -0
- lumera-0.31.0/lumera/releases/__init__.py +46 -0
- lumera-0.31.0/lumera/releases/__main__.py +178 -0
- lumera-0.31.0/lumera/releases/_api_child.py +36 -0
- lumera-0.31.0/lumera/releases/_archive.py +325 -0
- lumera-0.31.0/lumera/releases/_builder.py +375 -0
- lumera-0.31.0/lumera/releases/_config.py +142 -0
- lumera-0.31.0/lumera/releases/_contracts.py +185 -0
- lumera-0.31.0/lumera/releases/_errors.py +44 -0
- lumera-0.31.0/lumera/releases/_runtime.py +178 -0
- lumera-0.31.0/lumera/releases/_runtime_child.py +46 -0
- lumera-0.31.0/lumera/releases/_schemas.py +111 -0
- lumera-0.31.0/lumera/releases/_target.py +201 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera.egg-info/PKG-INFO +8 -1
- lumera-0.31.0/lumera.egg-info/SOURCES.txt +62 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera.egg-info/requires.txt +9 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/pyproject.toml +12 -1
- lumera-0.31.0/tests/test_function_resources.py +58 -0
- lumera-0.31.0/tests/test_integrations_google.py +19 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/tests/test_lazy_imports.py +115 -0
- lumera-0.28.0.dev2/lumera.egg-info/SOURCES.txt +0 -35
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/_live.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/_utils.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/agents.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/automations.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/email.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/exceptions.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/files.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/flags.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/integrations/bill.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/integrations/netsuite.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/integrations/salesforce.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/integrations/slack.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/llm.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/pb.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/sdk.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/storage.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera/webhooks.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera.egg-info/dependency_links.txt +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/lumera.egg-info/top_level.txt +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/setup.cfg +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/tests/test_agents_live.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/tests/test_automations.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/tests/test_flags.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/tests/test_integrations_netsuite.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/tests/test_integrations_salesforce.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/tests/test_sdk.py +0 -0
- {lumera-0.28.0.dev2 → lumera-0.31.0}/tests/test_webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lumera
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.0
|
|
4
4
|
Summary: SDK for building on Lumera platform
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: requests
|
|
@@ -11,6 +11,13 @@ Requires-Dist: slack_sdk==3.35.0
|
|
|
11
11
|
Provides-Extra: dev
|
|
12
12
|
Requires-Dist: ruff; extra == "dev"
|
|
13
13
|
Requires-Dist: pytest; extra == "dev"
|
|
14
|
+
Requires-Dist: pydantic<2.14.0,>=2.12.3; extra == "dev"
|
|
15
|
+
Requires-Dist: jsonschema[format-nongpl]<5.0.0,>=4.25.0; extra == "dev"
|
|
16
|
+
Provides-Extra: functions
|
|
17
|
+
Requires-Dist: pydantic<2.14.0,>=2.12.3; extra == "functions"
|
|
18
|
+
Requires-Dist: jsonschema[format-nongpl]<5.0.0,>=4.25.0; extra == "functions"
|
|
19
|
+
Provides-Extra: release
|
|
20
|
+
Requires-Dist: jsonschema[format-nongpl]<5.0.0,>=4.25.0; extra == "release"
|
|
14
21
|
Provides-Extra: full
|
|
15
22
|
Requires-Dist: airtable-python-wrapper==0.15.3; extra == "full"
|
|
16
23
|
Requires-Dist: boto3==1.39.3; extra == "full"
|
|
@@ -7,6 +7,26 @@ to interact with the Lumera API and define dynamic user interfaces.
|
|
|
7
7
|
|
|
8
8
|
from importlib import import_module as _import_module
|
|
9
9
|
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
# Keep the Functions authoring surface visible to static analyzers without
|
|
14
|
+
# eagerly importing it when users import the package root.
|
|
15
|
+
from .functions import (
|
|
16
|
+
ArtifactReference as ArtifactReference,
|
|
17
|
+
)
|
|
18
|
+
from .functions import (
|
|
19
|
+
FunctionContext as FunctionContext,
|
|
20
|
+
)
|
|
21
|
+
from .functions import (
|
|
22
|
+
FunctionError as FunctionError,
|
|
23
|
+
)
|
|
24
|
+
from .functions import (
|
|
25
|
+
HttpProjection as HttpProjection,
|
|
26
|
+
)
|
|
27
|
+
from .functions import (
|
|
28
|
+
function as function,
|
|
29
|
+
)
|
|
10
30
|
|
|
11
31
|
try:
|
|
12
32
|
__version__ = version("lumera")
|
|
@@ -89,6 +109,8 @@ _LAZY_MODULES = {
|
|
|
89
109
|
"exceptions": ".exceptions",
|
|
90
110
|
"files": ".files",
|
|
91
111
|
"flags": ".flags",
|
|
112
|
+
"functions": ".functions",
|
|
113
|
+
"function_resources": ".function_resources",
|
|
92
114
|
"integrations": ".integrations",
|
|
93
115
|
"llm": ".llm",
|
|
94
116
|
"pb": ".pb",
|
|
@@ -97,6 +119,8 @@ _LAZY_MODULES = {
|
|
|
97
119
|
"webhooks": ".webhooks",
|
|
98
120
|
}
|
|
99
121
|
|
|
122
|
+
_FUNCTIONS_OPTIONAL_DEPENDENCIES = frozenset({"jsonschema", "pydantic", "pydantic_core"})
|
|
123
|
+
|
|
100
124
|
_LAZY_ATTRIBUTES = {
|
|
101
125
|
# ._utils
|
|
102
126
|
"Credentials": ("._utils", "Credentials"),
|
|
@@ -124,6 +148,12 @@ _LAZY_ATTRIBUTES = {
|
|
|
124
148
|
"feature_flag": (".flags", "feature_flag"),
|
|
125
149
|
"feature_flags": (".flags", "feature_flags"),
|
|
126
150
|
"is_feature_enabled": (".flags", "is_feature_enabled"),
|
|
151
|
+
# .functions
|
|
152
|
+
"ArtifactReference": (".functions", "ArtifactReference"),
|
|
153
|
+
"function": (".functions", "function"),
|
|
154
|
+
"FunctionContext": (".functions", "FunctionContext"),
|
|
155
|
+
"FunctionError": (".functions", "FunctionError"),
|
|
156
|
+
"HttpProjection": (".functions", "HttpProjection"),
|
|
127
157
|
# .sdk
|
|
128
158
|
"CollectionField": (".sdk", "CollectionField"),
|
|
129
159
|
"HookReplayResult": (".sdk", "HookReplayResult"),
|
|
@@ -155,7 +185,20 @@ def __getattr__(name: str) -> object:
|
|
|
155
185
|
|
|
156
186
|
module_name = _LAZY_MODULES.get(name)
|
|
157
187
|
if module_name is not None:
|
|
158
|
-
|
|
188
|
+
try:
|
|
189
|
+
value = _import_module(module_name, __name__)
|
|
190
|
+
except ModuleNotFoundError as exc:
|
|
191
|
+
# ``functions`` is intentionally advertised for interactive
|
|
192
|
+
# discovery even though its authoring dependencies live behind an
|
|
193
|
+
# extra. Attribute-based discovery should be able to skip that
|
|
194
|
+
# optional module on a base install. Preserve every other import
|
|
195
|
+
# failure so broken installations are not mistaken for an absent
|
|
196
|
+
# optional feature.
|
|
197
|
+
if name == "functions" and exc.name in _FUNCTIONS_OPTIONAL_DEPENDENCIES:
|
|
198
|
+
raise AttributeError(
|
|
199
|
+
f"module {__name__!r} has no available attribute {name!r}; {exc}"
|
|
200
|
+
) from exc
|
|
201
|
+
raise
|
|
159
202
|
globals()[name] = value
|
|
160
203
|
return value
|
|
161
204
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Client helpers for immutable project Function resources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
from ._utils import _api_request
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def deploy_release(
|
|
14
|
+
project: str,
|
|
15
|
+
archive: str | Path,
|
|
16
|
+
*,
|
|
17
|
+
expected_digest: str | None = None,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
"""Upload and ingest a verified release without activating it."""
|
|
20
|
+
|
|
21
|
+
project_ref = _required_ref(project, "project")
|
|
22
|
+
archive_path = Path(archive).expanduser().resolve()
|
|
23
|
+
params = {"expected_digest": expected_digest} if expected_digest else None
|
|
24
|
+
with archive_path.open("rb") as stream:
|
|
25
|
+
payload = _api_request(
|
|
26
|
+
"POST",
|
|
27
|
+
f"projects/{quote(project_ref, safe='')}/runtime-releases",
|
|
28
|
+
params=params,
|
|
29
|
+
files={"release": (archive_path.name, stream, "application/gzip")},
|
|
30
|
+
timeout=600,
|
|
31
|
+
)
|
|
32
|
+
return _mapping(payload, "release deployment")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def list_runtime_releases(
|
|
36
|
+
project: str, *, cursor: str | None = None, limit: int = 50
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
return _list("runtime-releases", project, cursor=cursor, limit=limit)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def list_functions(project: str, *, cursor: str | None = None, limit: int = 50) -> dict[str, Any]:
|
|
42
|
+
return _list("functions", project, cursor=cursor, limit=limit)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_function(project: str, function: str) -> dict[str, Any]:
|
|
46
|
+
project_ref = _required_ref(project, "project")
|
|
47
|
+
function_ref = _required_ref(function, "function")
|
|
48
|
+
payload = _api_request(
|
|
49
|
+
"GET",
|
|
50
|
+
f"projects/{quote(project_ref, safe='')}/functions/{quote(function_ref, safe='')}",
|
|
51
|
+
)
|
|
52
|
+
return _mapping(payload, "Function")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def list_function_versions(
|
|
56
|
+
project: str,
|
|
57
|
+
function: str,
|
|
58
|
+
*,
|
|
59
|
+
cursor: str | None = None,
|
|
60
|
+
limit: int = 50,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
project_ref = _required_ref(project, "project")
|
|
63
|
+
function_ref = _required_ref(function, "function")
|
|
64
|
+
payload = _api_request(
|
|
65
|
+
"GET",
|
|
66
|
+
f"projects/{quote(project_ref, safe='')}/functions/{quote(function_ref, safe='')}/versions",
|
|
67
|
+
params=_page_params(cursor, limit),
|
|
68
|
+
)
|
|
69
|
+
return _mapping(payload, "Function versions")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _list(kind: str, project: str, *, cursor: str | None, limit: int) -> dict[str, Any]:
|
|
73
|
+
project_ref = _required_ref(project, "project")
|
|
74
|
+
payload = _api_request(
|
|
75
|
+
"GET",
|
|
76
|
+
f"projects/{quote(project_ref, safe='')}/{kind}",
|
|
77
|
+
params=_page_params(cursor, limit),
|
|
78
|
+
)
|
|
79
|
+
return _mapping(payload, kind)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _page_params(cursor: str | None, limit: int) -> dict[str, object]:
|
|
83
|
+
if not 1 <= limit <= 100:
|
|
84
|
+
raise ValueError("limit must be between 1 and 100")
|
|
85
|
+
params: dict[str, object] = {"limit": limit}
|
|
86
|
+
if cursor:
|
|
87
|
+
params["cursor"] = cursor
|
|
88
|
+
return params
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _required_ref(value: str, kind: str) -> str:
|
|
92
|
+
normalized = value.strip()
|
|
93
|
+
if not normalized:
|
|
94
|
+
raise ValueError(f"{kind} reference is required")
|
|
95
|
+
return normalized
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _mapping(payload: object | None, kind: str) -> dict[str, Any]:
|
|
99
|
+
if not isinstance(payload, Mapping):
|
|
100
|
+
raise RuntimeError(f"Lumera returned an invalid {kind} response")
|
|
101
|
+
return dict(payload)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"deploy_release",
|
|
106
|
+
"get_function",
|
|
107
|
+
"list_function_versions",
|
|
108
|
+
"list_functions",
|
|
109
|
+
"list_runtime_releases",
|
|
110
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Typed Lumera Functions authoring and local-composition API."""
|
|
2
|
+
|
|
3
|
+
# Keep the base SDK lightweight while making an accidental direct Functions
|
|
4
|
+
# import fail with installation guidance instead of an opaque dependency name.
|
|
5
|
+
try:
|
|
6
|
+
import jsonschema as _jsonschema
|
|
7
|
+
import pydantic as _pydantic
|
|
8
|
+
import pydantic_core as _pydantic_core
|
|
9
|
+
except ModuleNotFoundError as exc:
|
|
10
|
+
if exc.name not in {"jsonschema", "pydantic", "pydantic_core"}:
|
|
11
|
+
raise
|
|
12
|
+
raise ModuleNotFoundError(
|
|
13
|
+
"Lumera Functions dependencies are not installed; "
|
|
14
|
+
"install them with `pip install 'lumera[functions]'`",
|
|
15
|
+
name=exc.name,
|
|
16
|
+
) from exc
|
|
17
|
+
else:
|
|
18
|
+
del _jsonschema, _pydantic, _pydantic_core
|
|
19
|
+
|
|
20
|
+
from ._artifacts import ArtifactReference
|
|
21
|
+
from ._context import (
|
|
22
|
+
ActorIdentity,
|
|
23
|
+
AgentIdentity,
|
|
24
|
+
CancellationToken,
|
|
25
|
+
FunctionCaller,
|
|
26
|
+
FunctionContext,
|
|
27
|
+
ScopedLumeraClient,
|
|
28
|
+
StructuredLogger,
|
|
29
|
+
TraceContext,
|
|
30
|
+
)
|
|
31
|
+
from ._contract import (
|
|
32
|
+
FUNCTION_MANIFEST_SCHEMA_VERSION,
|
|
33
|
+
FunctionDeclaration,
|
|
34
|
+
FunctionDefinition,
|
|
35
|
+
FunctionEffect,
|
|
36
|
+
FunctionManifest,
|
|
37
|
+
FunctionMetadata,
|
|
38
|
+
HttpProjection,
|
|
39
|
+
function,
|
|
40
|
+
)
|
|
41
|
+
from ._errors import (
|
|
42
|
+
DuplicateFunctionError,
|
|
43
|
+
DuplicateHttpProjectionError,
|
|
44
|
+
DurableTransportUnavailableError,
|
|
45
|
+
DurableWaitNotAllowedError,
|
|
46
|
+
FunctionCallModeError,
|
|
47
|
+
FunctionCancelledError,
|
|
48
|
+
FunctionCycleError,
|
|
49
|
+
FunctionDeadlineExceededError,
|
|
50
|
+
FunctionDefinitionError,
|
|
51
|
+
FunctionDepthExceededError,
|
|
52
|
+
FunctionDiscoveryError,
|
|
53
|
+
FunctionError,
|
|
54
|
+
FunctionInvocationError,
|
|
55
|
+
FunctionNotFoundError,
|
|
56
|
+
FunctionOutputSerializationError,
|
|
57
|
+
FunctionRuntimeClosedError,
|
|
58
|
+
FunctionsError,
|
|
59
|
+
FunctionValidationError,
|
|
60
|
+
InvalidDurableWaitError,
|
|
61
|
+
UnexpectedFunctionError,
|
|
62
|
+
)
|
|
63
|
+
from ._execution import (
|
|
64
|
+
DurableFunctionTransport,
|
|
65
|
+
ExecutionHandle,
|
|
66
|
+
ExecutionStatus,
|
|
67
|
+
StartedExecution,
|
|
68
|
+
)
|
|
69
|
+
from ._registry import FunctionRegistry, discover_modules
|
|
70
|
+
from ._runtime import LocalFunctionRuntime, invoke_local
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"ActorIdentity",
|
|
74
|
+
"AgentIdentity",
|
|
75
|
+
"ArtifactReference",
|
|
76
|
+
"CancellationToken",
|
|
77
|
+
"DuplicateFunctionError",
|
|
78
|
+
"DuplicateHttpProjectionError",
|
|
79
|
+
"DurableFunctionTransport",
|
|
80
|
+
"DurableTransportUnavailableError",
|
|
81
|
+
"DurableWaitNotAllowedError",
|
|
82
|
+
"ExecutionHandle",
|
|
83
|
+
"ExecutionStatus",
|
|
84
|
+
"FUNCTION_MANIFEST_SCHEMA_VERSION",
|
|
85
|
+
"FunctionCallModeError",
|
|
86
|
+
"FunctionCaller",
|
|
87
|
+
"FunctionCancelledError",
|
|
88
|
+
"FunctionContext",
|
|
89
|
+
"FunctionCycleError",
|
|
90
|
+
"FunctionDeadlineExceededError",
|
|
91
|
+
"FunctionDeclaration",
|
|
92
|
+
"FunctionDefinition",
|
|
93
|
+
"FunctionDefinitionError",
|
|
94
|
+
"FunctionDepthExceededError",
|
|
95
|
+
"FunctionDiscoveryError",
|
|
96
|
+
"FunctionEffect",
|
|
97
|
+
"FunctionError",
|
|
98
|
+
"FunctionInvocationError",
|
|
99
|
+
"FunctionManifest",
|
|
100
|
+
"FunctionMetadata",
|
|
101
|
+
"FunctionNotFoundError",
|
|
102
|
+
"FunctionOutputSerializationError",
|
|
103
|
+
"FunctionRegistry",
|
|
104
|
+
"FunctionRuntimeClosedError",
|
|
105
|
+
"FunctionsError",
|
|
106
|
+
"FunctionValidationError",
|
|
107
|
+
"HttpProjection",
|
|
108
|
+
"InvalidDurableWaitError",
|
|
109
|
+
"LocalFunctionRuntime",
|
|
110
|
+
"ScopedLumeraClient",
|
|
111
|
+
"StartedExecution",
|
|
112
|
+
"StructuredLogger",
|
|
113
|
+
"TraceContext",
|
|
114
|
+
"UnexpectedFunctionError",
|
|
115
|
+
"discover_modules",
|
|
116
|
+
"function",
|
|
117
|
+
"invoke_local",
|
|
118
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Transport-neutral artifact references for Function boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ArtifactReference(BaseModel):
|
|
11
|
+
"""An opaque, non-authoritative reference to a Lumera-managed artifact.
|
|
12
|
+
|
|
13
|
+
Function payloads carry this JSON-safe envelope across local, API, durable,
|
|
14
|
+
and sharing boundaries. Runtime adapters are responsible for authorizing
|
|
15
|
+
and materializing the referenced artifact. Local paths, object-store keys,
|
|
16
|
+
URLs, and credentials are deliberately absent from the contract.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
20
|
+
|
|
21
|
+
kind: Literal["lumera_artifact"]
|
|
22
|
+
artifact_id: str = Field(
|
|
23
|
+
min_length=1,
|
|
24
|
+
max_length=512,
|
|
25
|
+
pattern=r"^\S+$",
|
|
26
|
+
description="Opaque Lumera artifact identifier",
|
|
27
|
+
)
|
|
28
|
+
filename: str | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
min_length=1,
|
|
31
|
+
max_length=1024,
|
|
32
|
+
description="Non-authoritative display filename",
|
|
33
|
+
)
|
|
34
|
+
content_type: str | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
min_length=1,
|
|
37
|
+
max_length=255,
|
|
38
|
+
description="Non-authoritative media type",
|
|
39
|
+
)
|
|
40
|
+
size_bytes: int | None = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
ge=0,
|
|
43
|
+
description="Non-authoritative byte size",
|
|
44
|
+
)
|
|
45
|
+
sha256: str | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
pattern=r"^[0-9a-f]{64}$",
|
|
48
|
+
description="Optional lowercase SHA-256 content digest",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["ArtifactReference"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Local project configuration for explicit Function module discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import tomllib
|
|
11
|
+
|
|
12
|
+
from ._errors import FunctionDiscoveryError
|
|
13
|
+
|
|
14
|
+
_MODULE_PATTERN = re.compile(r"^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class FunctionProjectConfig:
|
|
19
|
+
"""Resolved ``[tool.lumera.functions]`` configuration."""
|
|
20
|
+
|
|
21
|
+
project_root: Path
|
|
22
|
+
source_root: Path
|
|
23
|
+
modules: tuple[str, ...]
|
|
24
|
+
test_paths: tuple[Path, ...]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_project_config(
|
|
28
|
+
project_root: str | Path,
|
|
29
|
+
*,
|
|
30
|
+
module_overrides: list[str] | None = None,
|
|
31
|
+
) -> FunctionProjectConfig:
|
|
32
|
+
root = Path(project_root).expanduser().resolve()
|
|
33
|
+
if not root.is_dir():
|
|
34
|
+
raise FunctionDiscoveryError(f"project root is not a directory: {root}")
|
|
35
|
+
config_path = root / "pyproject.toml"
|
|
36
|
+
if not config_path.is_file():
|
|
37
|
+
raise FunctionDiscoveryError(f"missing {config_path}; configure [tool.lumera.functions]")
|
|
38
|
+
try:
|
|
39
|
+
document = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
40
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
41
|
+
raise FunctionDiscoveryError(f"could not read {config_path}") from exc
|
|
42
|
+
|
|
43
|
+
raw_config = _functions_config(document)
|
|
44
|
+
raw_source_root = raw_config.get("source-root", "platform")
|
|
45
|
+
if not isinstance(raw_source_root, str) or not raw_source_root.strip():
|
|
46
|
+
raise FunctionDiscoveryError("tool.lumera.functions.source-root must be a path")
|
|
47
|
+
source_root = (root / raw_source_root).resolve()
|
|
48
|
+
if not source_root.is_relative_to(root):
|
|
49
|
+
raise FunctionDiscoveryError("Function source-root must stay inside the project")
|
|
50
|
+
if not source_root.is_dir():
|
|
51
|
+
raise FunctionDiscoveryError(f"Function source-root does not exist: {source_root}")
|
|
52
|
+
|
|
53
|
+
raw_modules: Any = module_overrides if module_overrides else raw_config.get("modules", [])
|
|
54
|
+
if not isinstance(raw_modules, list) or not all(
|
|
55
|
+
isinstance(module, str) for module in raw_modules
|
|
56
|
+
):
|
|
57
|
+
raise FunctionDiscoveryError("tool.lumera.functions.modules must be a string array")
|
|
58
|
+
modules: list[str] = []
|
|
59
|
+
for raw_module in raw_modules:
|
|
60
|
+
module = raw_module.strip()
|
|
61
|
+
if not module or not _MODULE_PATTERN.fullmatch(module):
|
|
62
|
+
raise FunctionDiscoveryError(f"invalid Function module name: {raw_module!r}")
|
|
63
|
+
_require_project_module(module, source_root)
|
|
64
|
+
if module not in modules:
|
|
65
|
+
modules.append(module)
|
|
66
|
+
|
|
67
|
+
raw_test_paths = raw_config.get("test-paths", ["tests/functions"])
|
|
68
|
+
if not isinstance(raw_test_paths, list) or not all(
|
|
69
|
+
isinstance(path, str) for path in raw_test_paths
|
|
70
|
+
):
|
|
71
|
+
raise FunctionDiscoveryError("tool.lumera.functions.test-paths must be a string array")
|
|
72
|
+
test_paths: list[Path] = []
|
|
73
|
+
for raw_path in raw_test_paths:
|
|
74
|
+
test_path = (root / raw_path).resolve()
|
|
75
|
+
if not test_path.is_relative_to(root):
|
|
76
|
+
raise FunctionDiscoveryError("Function test paths must stay inside the project")
|
|
77
|
+
test_paths.append(test_path)
|
|
78
|
+
|
|
79
|
+
return FunctionProjectConfig(
|
|
80
|
+
project_root=root,
|
|
81
|
+
source_root=source_root,
|
|
82
|
+
modules=tuple(modules),
|
|
83
|
+
test_paths=tuple(test_paths),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _functions_config(document: dict[str, Any]) -> dict[str, Any]:
|
|
88
|
+
value: Any = document
|
|
89
|
+
for key in ("tool", "lumera", "functions"):
|
|
90
|
+
if not isinstance(value, dict):
|
|
91
|
+
return {}
|
|
92
|
+
value = value.get(key, {})
|
|
93
|
+
if not isinstance(value, dict):
|
|
94
|
+
raise FunctionDiscoveryError("tool.lumera.functions must be a TOML table")
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _require_project_module(module: str, source_root: Path) -> None:
|
|
99
|
+
"""Require an explicit module to resolve to Python source under source-root."""
|
|
100
|
+
|
|
101
|
+
relative_module = Path(*module.split("."))
|
|
102
|
+
candidates = (
|
|
103
|
+
(source_root / relative_module).with_suffix(".py"),
|
|
104
|
+
source_root / relative_module / "__init__.py",
|
|
105
|
+
)
|
|
106
|
+
for candidate in candidates:
|
|
107
|
+
resolved = candidate.resolve()
|
|
108
|
+
if resolved.is_relative_to(source_root) and resolved.is_file():
|
|
109
|
+
return
|
|
110
|
+
raise FunctionDiscoveryError(f"Function module must resolve inside source-root: {module!r}")
|