flowspec2 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowspec2/__init__.py +105 -0
- flowspec2/authoring/__init__.py +275 -0
- flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
- flowspec2/authoring/authoring-evidence.schema.json +455 -0
- flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
- flowspec2/authoring/benchmark.py +1409 -0
- flowspec2/authoring/corpus/await_correction.case.json +220 -0
- flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
- flowspec2/authoring/corpus/gated_derive.case.json +100 -0
- flowspec2/authoring/corpus/linear.case.json +55 -0
- flowspec2/authoring/corpus/manifest.json +31 -0
- flowspec2/authoring/corpus/subflow.case.json +66 -0
- flowspec2/authoring/corpus/terminal.case.json +94 -0
- flowspec2/authoring/corpus.py +307 -0
- flowspec2/authoring/ctk.py +836 -0
- flowspec2/authoring/detached_signature.py +120 -0
- flowspec2/authoring/evidence.py +311 -0
- flowspec2/authoring/evidence_signature.py +187 -0
- flowspec2/authoring/evidence_verification.py +229 -0
- flowspec2/authoring/gemini.py +215 -0
- flowspec2/authoring/operational-corpus.json +61 -0
- flowspec2/authoring/operational.py +956 -0
- flowspec2/authoring/operational_providers.py +167 -0
- flowspec2/authoring/presentation_review.py +1013 -0
- flowspec2/authoring/presentation_review_signature.py +305 -0
- flowspec2/authoring/projection.py +299 -0
- flowspec2/authoring/provider_prompt.py +83 -0
- flowspec2/backends/__init__.py +82 -0
- flowspec2/backends/http.py +189 -0
- flowspec2/checker.py +238 -0
- flowspec2/cli.py +789 -0
- flowspec2/cli_parser.py +345 -0
- flowspec2/clock.py +23 -0
- flowspec2/compat/__init__.py +47 -0
- flowspec2/compat/models.py +82 -0
- flowspec2/compat/open_workflow.py +616 -0
- flowspec2/compat/rasa.py +19 -0
- flowspec2/compat/rasa_export.py +876 -0
- flowspec2/compat/rasa_import.py +992 -0
- flowspec2/compat/rasa_shared.py +270 -0
- flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
- flowspec2/compat/tool_profiles.py +149 -0
- flowspec2/compat/yaml.py +147 -0
- flowspec2/compiler.py +957 -0
- flowspec2/compiler_contracts.py +17 -0
- flowspec2/compiler_resume_contracts.py +594 -0
- flowspec2/compiler_schema_relations.py +1830 -0
- flowspec2/compiler_tool_contracts.py +245 -0
- flowspec2/compiler_value_contracts.py +447 -0
- flowspec2/derive.py +32 -0
- flowspec2/diagnostics.py +94 -0
- flowspec2/domains.py +489 -0
- flowspec2/experimental/__init__.py +57 -0
- flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
- flowspec2/experimental/v3-preview-loss-policy.json +161 -0
- flowspec2/experimental/v3_lowering.py +928 -0
- flowspec2/experimental/v3_preview.py +2460 -0
- flowspec2/flowspec-2.schema.json +635 -0
- flowspec2/interactive.py +300 -0
- flowspec2/ir.py +1459 -0
- flowspec2/json_codec.py +120 -0
- flowspec2/llm.py +366 -0
- flowspec2/models.py +121 -0
- flowspec2/nodes.py +1537 -0
- flowspec2/observability.py +151 -0
- flowspec2/predicates.py +89 -0
- flowspec2/profiles.py +118 -0
- flowspec2/py.typed +0 -0
- flowspec2/runtime.py +1059 -0
- flowspec2/schema.py +54 -0
- flowspec2/schema_contracts.py +203 -0
- flowspec2/semantic_derive_contracts.py +530 -0
- flowspec2/semantic_path_contracts.py +528 -0
- flowspec2/semantic_predicate_contracts.py +355 -0
- flowspec2/semantic_schema_contracts.py +137 -0
- flowspec2/semantic_source_contracts.py +21 -0
- flowspec2/semantic_state_contracts.py +450 -0
- flowspec2/semantic_support.py +40 -0
- flowspec2/semantics.py +410 -0
- flowspec2/state_migration.py +1034 -0
- flowspec2/subflows/__init__.py +1117 -0
- flowspec2/subflows/address.py +328 -0
- flowspec2/subflows/identification.py +1031 -0
- flowspec2/tools.py +1154 -0
- flowspec2-1.0.0.dist-info/METADATA +482 -0
- flowspec2-1.0.0.dist-info/RECORD +92 -0
- flowspec2-1.0.0.dist-info/WHEEL +4 -0
- flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
- flowspec2-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Snowflake correlation IDs and structured logging helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import socket
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable, Mapping
|
|
11
|
+
from threading import Lock
|
|
12
|
+
|
|
13
|
+
SNOWFLAKE_EPOCH_MILLISECONDS = 1_704_067_200_000
|
|
14
|
+
SNOWFLAKE_TIMESTAMP_BITS = 41
|
|
15
|
+
SNOWFLAKE_WORKER_BITS = 10
|
|
16
|
+
SNOWFLAKE_SEQUENCE_BITS = 12
|
|
17
|
+
SNOWFLAKE_MAX_WORKER_ID = (1 << SNOWFLAKE_WORKER_BITS) - 1
|
|
18
|
+
SNOWFLAKE_MAX_SEQUENCE = (1 << SNOWFLAKE_SEQUENCE_BITS) - 1
|
|
19
|
+
SNOWFLAKE_MAX_TIMESTAMP_DELTA = (1 << SNOWFLAKE_TIMESTAMP_BITS) - 1
|
|
20
|
+
SNOWFLAKE_WORKER_SHIFT = SNOWFLAKE_SEQUENCE_BITS
|
|
21
|
+
SNOWFLAKE_TIMESTAMP_SHIFT = SNOWFLAKE_WORKER_BITS + SNOWFLAKE_SEQUENCE_BITS
|
|
22
|
+
|
|
23
|
+
ClockMilliseconds = Callable[[], int]
|
|
24
|
+
_LOG_RECORD_RESERVED_FIELDS = frozenset(logging.makeLogRecord({}).__dict__) | {
|
|
25
|
+
"asctime",
|
|
26
|
+
"log_id",
|
|
27
|
+
"message",
|
|
28
|
+
"operation",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _system_clock_milliseconds() -> int:
|
|
33
|
+
return time.time_ns() // 1_000_000
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SnowflakeIdGenerator:
|
|
37
|
+
"""Generate ordered decimal Snowflake IDs with an injectable clock."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
worker_id: int,
|
|
43
|
+
clock_milliseconds: ClockMilliseconds = _system_clock_milliseconds,
|
|
44
|
+
epoch_milliseconds: int = SNOWFLAKE_EPOCH_MILLISECONDS,
|
|
45
|
+
) -> None:
|
|
46
|
+
if isinstance(worker_id, bool) or not 0 <= worker_id <= SNOWFLAKE_MAX_WORKER_ID:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"worker_id must be between 0 and {SNOWFLAKE_MAX_WORKER_ID}, got {worker_id!r}"
|
|
49
|
+
)
|
|
50
|
+
if isinstance(epoch_milliseconds, bool) or epoch_milliseconds < 0:
|
|
51
|
+
raise ValueError("epoch_milliseconds must be a non-negative integer")
|
|
52
|
+
self.worker_id = worker_id
|
|
53
|
+
self._clock_milliseconds = clock_milliseconds
|
|
54
|
+
self._epoch_milliseconds = epoch_milliseconds
|
|
55
|
+
self._last_timestamp_milliseconds: int | None = None
|
|
56
|
+
self._sequence = 0
|
|
57
|
+
self._lock = Lock()
|
|
58
|
+
|
|
59
|
+
def generate(self) -> str:
|
|
60
|
+
"""Return the next ID, advancing logical time across clock rollback or saturation."""
|
|
61
|
+
|
|
62
|
+
with self._lock:
|
|
63
|
+
clock_timestamp = self._clock_milliseconds()
|
|
64
|
+
if isinstance(clock_timestamp, bool) or not isinstance(clock_timestamp, int):
|
|
65
|
+
raise TypeError("clock_milliseconds must return an integer")
|
|
66
|
+
if clock_timestamp < self._epoch_milliseconds:
|
|
67
|
+
raise ValueError("clock timestamp precedes the configured Snowflake epoch")
|
|
68
|
+
|
|
69
|
+
last_timestamp = self._last_timestamp_milliseconds
|
|
70
|
+
logical_timestamp = (
|
|
71
|
+
clock_timestamp if last_timestamp is None else max(clock_timestamp, last_timestamp)
|
|
72
|
+
)
|
|
73
|
+
if last_timestamp is not None and logical_timestamp == last_timestamp:
|
|
74
|
+
self._sequence += 1
|
|
75
|
+
if self._sequence > SNOWFLAKE_MAX_SEQUENCE:
|
|
76
|
+
logical_timestamp += 1
|
|
77
|
+
self._sequence = 0
|
|
78
|
+
else:
|
|
79
|
+
self._sequence = 0
|
|
80
|
+
|
|
81
|
+
timestamp_delta = logical_timestamp - self._epoch_milliseconds
|
|
82
|
+
if timestamp_delta > SNOWFLAKE_MAX_TIMESTAMP_DELTA:
|
|
83
|
+
raise OverflowError("Snowflake timestamp exceeds the configured bit allocation")
|
|
84
|
+
|
|
85
|
+
self._last_timestamp_milliseconds = logical_timestamp
|
|
86
|
+
snowflake_id = (
|
|
87
|
+
(timestamp_delta << SNOWFLAKE_TIMESTAMP_SHIFT)
|
|
88
|
+
| (self.worker_id << SNOWFLAKE_WORKER_SHIFT)
|
|
89
|
+
| self._sequence
|
|
90
|
+
)
|
|
91
|
+
return str(snowflake_id)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _configured_worker_id() -> int:
|
|
95
|
+
configured_worker_id = os.environ.get("FLOWSPEC2_SNOWFLAKE_WORKER_ID")
|
|
96
|
+
if configured_worker_id is not None:
|
|
97
|
+
try:
|
|
98
|
+
worker_id = int(configured_worker_id)
|
|
99
|
+
except ValueError as error:
|
|
100
|
+
raise ValueError("FLOWSPEC2_SNOWFLAKE_WORKER_ID must be an integer") from error
|
|
101
|
+
if not 0 <= worker_id <= SNOWFLAKE_MAX_WORKER_ID:
|
|
102
|
+
raise ValueError("FLOWSPEC2_SNOWFLAKE_WORKER_ID is outside the supported worker range")
|
|
103
|
+
return worker_id
|
|
104
|
+
|
|
105
|
+
process_identity = f"{socket.gethostname()}:{os.getpid()}".encode("utf-8")
|
|
106
|
+
identity_digest = hashlib.blake2s(process_identity, digest_size=2).digest()
|
|
107
|
+
return int.from_bytes(identity_digest, byteorder="big") & SNOWFLAKE_MAX_WORKER_ID
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_default_generator_lock = Lock()
|
|
111
|
+
_default_generator_process_id: int | None = None
|
|
112
|
+
_default_generator: SnowflakeIdGenerator | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def default_log_id_generator() -> SnowflakeIdGenerator:
|
|
116
|
+
"""Return the process-local generator, recreating it after a process fork."""
|
|
117
|
+
|
|
118
|
+
global _default_generator
|
|
119
|
+
global _default_generator_process_id
|
|
120
|
+
|
|
121
|
+
process_id = os.getpid()
|
|
122
|
+
with _default_generator_lock:
|
|
123
|
+
if _default_generator is None or _default_generator_process_id != process_id:
|
|
124
|
+
_default_generator = SnowflakeIdGenerator(worker_id=_configured_worker_id())
|
|
125
|
+
_default_generator_process_id = process_id
|
|
126
|
+
return _default_generator
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def log_event(
|
|
130
|
+
logger: logging.Logger,
|
|
131
|
+
level: int,
|
|
132
|
+
message: str,
|
|
133
|
+
*,
|
|
134
|
+
operation: str,
|
|
135
|
+
log_id_generator: SnowflakeIdGenerator | None = None,
|
|
136
|
+
context: Mapping[str, object] | None = None,
|
|
137
|
+
exc_info: bool = False,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""Emit one structured event and return its correlation ID."""
|
|
140
|
+
|
|
141
|
+
if context:
|
|
142
|
+
conflicting_fields = _LOG_RECORD_RESERVED_FIELDS.intersection(context)
|
|
143
|
+
if conflicting_fields:
|
|
144
|
+
conflicts = ", ".join(sorted(conflicting_fields))
|
|
145
|
+
raise ValueError(f"logging context cannot override reserved fields: {conflicts}")
|
|
146
|
+
log_id = (log_id_generator or default_log_id_generator()).generate()
|
|
147
|
+
extra: dict[str, object] = {"log_id": log_id, "operation": operation}
|
|
148
|
+
if context:
|
|
149
|
+
extra.update(context)
|
|
150
|
+
logger.log(level, message, extra=extra, exc_info=exc_info)
|
|
151
|
+
return log_id
|
flowspec2/predicates.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""The frozen object-form predicate grammar.
|
|
2
|
+
|
|
3
|
+
A predicate is a single-key object over five read-only namespaces. There is no
|
|
4
|
+
string sugar and no arbitrary computation — every gate, ``ask_when``,
|
|
5
|
+
``send_when`` and correction route compiles to a call to :func:`evaluate`, which
|
|
6
|
+
is a pure boolean function over ``ServiceState`` + the flow ``config`` block.
|
|
7
|
+
|
|
8
|
+
Grammar (one operator per object)::
|
|
9
|
+
|
|
10
|
+
{"in": ["slots.x", ["a", "b"]]} # resolved(x) in the literal list
|
|
11
|
+
{"eq": ["slots.x", "literal"]} # resolved(x) == literal (either side may be a ref)
|
|
12
|
+
{"eq": ["slots.x", {"literal": "config.flag"}]} # namespaced literal
|
|
13
|
+
{"ne": ["slots.x", "literal"]}
|
|
14
|
+
{"is_present": "slots.x"} # filled and non-null/non-empty
|
|
15
|
+
{"and": [<pred>, ...]}
|
|
16
|
+
{"or": [<pred>, ...]}
|
|
17
|
+
{"not": <pred>}
|
|
18
|
+
|
|
19
|
+
Namespaces: ``slots.`` (state.data), ``internal.`` (state.internal),
|
|
20
|
+
``payload.`` (state.payload), ``config.`` (the flow config block),
|
|
21
|
+
``address.`` (state.data["address"]).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, TypeGuard
|
|
27
|
+
|
|
28
|
+
from .models import ServiceState
|
|
29
|
+
|
|
30
|
+
NAMESPACES = ("slots", "internal", "payload", "config", "address")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_ref(value: Any) -> TypeGuard[str]:
|
|
34
|
+
return isinstance(value, str) and "." in value and value.split(".", 1)[0] in NAMESPACES
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve(ref: str, state: ServiceState, config: dict[str, Any]) -> Any:
|
|
38
|
+
ns, key = ref.split(".", 1)
|
|
39
|
+
if ns == "slots":
|
|
40
|
+
return state.data.get(key)
|
|
41
|
+
if ns == "internal":
|
|
42
|
+
return state.internal.get(key)
|
|
43
|
+
if ns == "payload":
|
|
44
|
+
return state.payload.get(key)
|
|
45
|
+
if ns == "config":
|
|
46
|
+
return config.get(key)
|
|
47
|
+
if ns == "address":
|
|
48
|
+
resolved: Any = state.data.get("address") or {}
|
|
49
|
+
for path_segment in key.split("."):
|
|
50
|
+
if not isinstance(resolved, dict) or path_segment not in resolved:
|
|
51
|
+
return None
|
|
52
|
+
resolved = resolved[path_segment]
|
|
53
|
+
return resolved
|
|
54
|
+
raise ValueError(f"unknown predicate namespace: {ns!r}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _operand(value: Any, state: ServiceState, config: dict[str, Any]) -> Any:
|
|
58
|
+
"""Resolve namespaced strings and unwrap explicit ambiguous literals."""
|
|
59
|
+
if isinstance(value, dict) and set(value) == {"literal"}:
|
|
60
|
+
return value["literal"]
|
|
61
|
+
return _resolve(value, state, config) if _is_ref(value) else value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def evaluate(pred: dict[str, Any], state: ServiceState, config: dict[str, Any]) -> bool:
|
|
65
|
+
"""Evaluate a predicate against state + config. Pure, side-effect free."""
|
|
66
|
+
if not isinstance(pred, dict) or len(pred) != 1:
|
|
67
|
+
raise ValueError(f"predicate must be a single-key object, got {pred!r}")
|
|
68
|
+
((op, val),) = pred.items()
|
|
69
|
+
|
|
70
|
+
if op == "in":
|
|
71
|
+
ref, options = val
|
|
72
|
+
return _operand(ref, state, config) in options
|
|
73
|
+
if op == "eq":
|
|
74
|
+
left, right = val
|
|
75
|
+
return bool(_operand(left, state, config) == _operand(right, state, config))
|
|
76
|
+
if op == "ne":
|
|
77
|
+
left, right = val
|
|
78
|
+
return bool(_operand(left, state, config) != _operand(right, state, config))
|
|
79
|
+
if op == "is_present":
|
|
80
|
+
resolved = _resolve(val, state, config)
|
|
81
|
+
return resolved is not None and resolved != ""
|
|
82
|
+
if op == "and":
|
|
83
|
+
return all(evaluate(p, state, config) for p in val)
|
|
84
|
+
if op == "or":
|
|
85
|
+
return any(evaluate(p, state, config) for p in val)
|
|
86
|
+
if op == "not":
|
|
87
|
+
return not evaluate(val, state, config)
|
|
88
|
+
|
|
89
|
+
raise ValueError(f"unknown predicate operator: {op!r}")
|
flowspec2/profiles.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Runtime profiles bind portable flow contracts to executable capabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Final
|
|
10
|
+
|
|
11
|
+
from .subflows import SubflowRegistry, default_subflows
|
|
12
|
+
from .tools import ToolRegistry, default_tool_registry
|
|
13
|
+
|
|
14
|
+
REFERENCE_PROFILE_ID: Final[str] = "flowspec2/reference@2"
|
|
15
|
+
REFERENCE_DOMAIN_TYPES: Final[frozenset[str]] = frozenset(
|
|
16
|
+
{"categorical", "bool", "free_text", "brazilian_tax_id", "email", "name", "integer", "number"}
|
|
17
|
+
)
|
|
18
|
+
REFERENCE_CAPABILITIES: Final[frozenset[str]] = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"auto_flow",
|
|
21
|
+
"await_external",
|
|
22
|
+
"external_authentication",
|
|
23
|
+
"geocoding",
|
|
24
|
+
"handoff",
|
|
25
|
+
"identity_lookup",
|
|
26
|
+
"location_in",
|
|
27
|
+
"media_in",
|
|
28
|
+
"media_out",
|
|
29
|
+
"session_reset",
|
|
30
|
+
"tts",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def capability_is_requested(capability_value: Any) -> bool:
|
|
36
|
+
"""Return whether a normalized capability value requests host support."""
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
capability_value is True
|
|
40
|
+
or isinstance(capability_value, Mapping)
|
|
41
|
+
and bool(capability_value)
|
|
42
|
+
or isinstance(capability_value, (list, tuple))
|
|
43
|
+
and bool(capability_value)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class FlowProfile:
|
|
49
|
+
"""Named registries and host features against which a flow is checked."""
|
|
50
|
+
|
|
51
|
+
identifier: str
|
|
52
|
+
tools: ToolRegistry
|
|
53
|
+
subflows: SubflowRegistry
|
|
54
|
+
capabilities: frozenset[str]
|
|
55
|
+
domain_types: frozenset[str]
|
|
56
|
+
allow_legacy_contracts: bool = False
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
if not self.identifier.strip():
|
|
60
|
+
raise ValueError("profile identifier must be non-empty")
|
|
61
|
+
if any(not capability.strip() for capability in self.capabilities):
|
|
62
|
+
raise ValueError("profile capabilities must be non-empty strings")
|
|
63
|
+
if any(not domain_type.strip() for domain_type in self.domain_types):
|
|
64
|
+
raise ValueError("profile domain types must be non-empty strings")
|
|
65
|
+
|
|
66
|
+
def as_dict(self) -> dict[str, Any]:
|
|
67
|
+
"""Return the complete owned runtime-profile contract."""
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"identifier": self.identifier,
|
|
71
|
+
"allow_legacy_contracts": self.allow_legacy_contracts,
|
|
72
|
+
"capabilities": sorted(self.capabilities),
|
|
73
|
+
"domain_types": sorted(self.domain_types),
|
|
74
|
+
"tools": {
|
|
75
|
+
tool_name: tool_definition.as_dict()
|
|
76
|
+
for tool_name, tool_definition in sorted(self.tools.definitions.items())
|
|
77
|
+
},
|
|
78
|
+
"subflows": {
|
|
79
|
+
subflow_reference: subflow_definition.as_dict()
|
|
80
|
+
for subflow_reference, subflow_definition in sorted(
|
|
81
|
+
self.subflows.definitions.items()
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def canonical_json(self) -> str:
|
|
87
|
+
"""Serialize the complete profile contract deterministically."""
|
|
88
|
+
|
|
89
|
+
return json.dumps(
|
|
90
|
+
self.as_dict(),
|
|
91
|
+
ensure_ascii=False,
|
|
92
|
+
allow_nan=False,
|
|
93
|
+
separators=(",", ":"),
|
|
94
|
+
sort_keys=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def digest(self) -> str:
|
|
99
|
+
"""Return the content identity used by IR and authoring provenance."""
|
|
100
|
+
|
|
101
|
+
return hashlib.sha256(self.canonical_json().encode("utf-8")).hexdigest()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def reference_profile(
|
|
105
|
+
*,
|
|
106
|
+
tools: ToolRegistry | None = None,
|
|
107
|
+
subflows: SubflowRegistry | None = None,
|
|
108
|
+
) -> FlowProfile:
|
|
109
|
+
"""Build the executable profile shipped with the reference runtime."""
|
|
110
|
+
|
|
111
|
+
return FlowProfile(
|
|
112
|
+
identifier=REFERENCE_PROFILE_ID,
|
|
113
|
+
tools=tools or default_tool_registry(),
|
|
114
|
+
subflows=subflows or default_subflows(),
|
|
115
|
+
capabilities=REFERENCE_CAPABILITIES,
|
|
116
|
+
domain_types=REFERENCE_DOMAIN_TYPES,
|
|
117
|
+
allow_legacy_contracts=False,
|
|
118
|
+
)
|
flowspec2/py.typed
ADDED
|
File without changes
|