velophex 0.2.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.
- velophex/__init__.py +38 -0
- velophex/_json.py +297 -0
- velophex/agents/__init__.py +130 -0
- velophex/agents/_errors.py +75 -0
- velophex/agents/_loop.py +436 -0
- velophex/agents/_tools.py +168 -0
- velophex/agents/_tracing.py +87 -0
- velophex/agents/testing.py +249 -0
- velophex/cli/__init__.py +5 -0
- velophex/cli/main.py +548 -0
- velophex/cli/publish.py +152 -0
- velophex/cli/sync.py +90 -0
- velophex/cli/templates.py +126 -0
- velophex/contracts/__init__.py +103 -0
- velophex/contracts/_model.py +26 -0
- velophex/contracts/agents.py +110 -0
- velophex/contracts/diagnostics.py +157 -0
- velophex/contracts/executor.py +1921 -0
- velophex/contracts/package.py +391 -0
- velophex/contracts/project.py +1008 -0
- velophex/contracts/runtimes.py +226 -0
- velophex/contracts/signin.py +53 -0
- velophex/contracts/validation.py +158 -0
- velophex/entrypoints.py +329 -0
- velophex/errors.py +44 -0
- velophex/packaging.py +465 -0
- velophex/py.typed +0 -0
- velophex/runtime/__init__.py +471 -0
- velophex/runtime/_session.py +192 -0
- velophex/runtime/client.py +957 -0
- velophex/runtime/models.py +326 -0
- velophex/signing.py +425 -0
- velophex/worker/__init__.py +27 -0
- velophex/worker/__main__.py +118 -0
- velophex/worker/framing.py +248 -0
- velophex/worker/loader.py +105 -0
- velophex/worker/outbox.py +147 -0
- velophex/worker/session.py +519 -0
- velophex-0.2.0.dist-info/METADATA +313 -0
- velophex-0.2.0.dist-info/RECORD +43 -0
- velophex-0.2.0.dist-info/WHEEL +4 -0
- velophex-0.2.0.dist-info/entry_points.txt +2 -0
- velophex-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Runtime kinds and the runner's shared vocabulary (phases, levels, fault kinds).
|
|
2
|
+
|
|
3
|
+
Mirrors ``VeloPhex.Platform.Contracts.Runtimes`` and ``…Contracts.Runner.RunnerVocabulary``.
|
|
4
|
+
A runtime kind stays an open token: the platform recognises a runtime by registering an
|
|
5
|
+
adapter for its kind, never by switching over a closed enum (ADR-P001), so this module
|
|
6
|
+
offers constants and a validator rather than an ``Enum``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Final
|
|
12
|
+
|
|
13
|
+
from velophex.contracts.validation import is_runtime_kind
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"MAXIMUM_RUNTIME_KIND_LENGTH",
|
|
17
|
+
"AttemptOutcomes",
|
|
18
|
+
"ControlSignals",
|
|
19
|
+
"ExecutionPhases",
|
|
20
|
+
"FaultCodes",
|
|
21
|
+
"FaultKinds",
|
|
22
|
+
"LogLevels",
|
|
23
|
+
"LogStages",
|
|
24
|
+
"PhaseStatuses",
|
|
25
|
+
"RuntimeKind",
|
|
26
|
+
"RuntimeStatuses",
|
|
27
|
+
"SignalReasons",
|
|
28
|
+
"parse_runtime_kind",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
MAXIMUM_RUNTIME_KIND_LENGTH: Final = 32
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RuntimeKind:
|
|
35
|
+
"""The runtime kinds the platform ships with."""
|
|
36
|
+
|
|
37
|
+
VELOPHEX_WORKFLOW: Final = "velophex-workflow"
|
|
38
|
+
PYTHON: Final = "python"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_runtime_kind(value: str | None) -> str:
|
|
42
|
+
"""Return *value* if it is a well-formed runtime kind, else raise :class:`ValueError`."""
|
|
43
|
+
if not is_runtime_kind(value):
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"{value!r} is not a runtime kind. Use a lowercase token such as 'velophex-workflow' or 'python'."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert value is not None
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RuntimeStatuses:
|
|
53
|
+
"""Health of a runtime a runner reports it has installed.
|
|
54
|
+
|
|
55
|
+
Two values, not three. A runtime the machine does not have is *absent* from the report, not
|
|
56
|
+
"Unavailable": the Orchestrator places a job only on a runner that reports the runtimes the
|
|
57
|
+
job needs, so a missing entry already says everything a third status could. ``IsKnown`` on the
|
|
58
|
+
.NET side rejects anything else, which means a reporter that invented one would be refused.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
AVAILABLE: Final = "Available"
|
|
62
|
+
DEGRADED: Final = "Degraded"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ExecutionPhases:
|
|
66
|
+
"""Runtime-neutral log phases. Runtime-specific detail belongs in ``subPhase``."""
|
|
67
|
+
|
|
68
|
+
DOWNLOAD: Final = "Download"
|
|
69
|
+
VERIFY: Final = "Verify"
|
|
70
|
+
EXTRACT: Final = "Extract"
|
|
71
|
+
ENVIRONMENT_PREPARE: Final = "EnvironmentPrepare"
|
|
72
|
+
DEPENDENCY_RESTORE: Final = "DependencyRestore"
|
|
73
|
+
EXECUTION: Final = "Execution"
|
|
74
|
+
CLEANUP: Final = "Cleanup"
|
|
75
|
+
|
|
76
|
+
ALL: Final = (DOWNLOAD, VERIFY, EXTRACT, ENVIRONMENT_PREPARE, DEPENDENCY_RESTORE, EXECUTION, CLEANUP)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class LogStages:
|
|
80
|
+
"""The two stages a log line belongs to; derived from the phase when it is omitted."""
|
|
81
|
+
|
|
82
|
+
PREPARATION: Final = "Preparation"
|
|
83
|
+
EXECUTION: Final = "Execution"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PhaseStatuses:
|
|
87
|
+
"""Status of a preparation phase."""
|
|
88
|
+
|
|
89
|
+
STARTED: Final = "Started"
|
|
90
|
+
SUCCEEDED: Final = "Succeeded"
|
|
91
|
+
FAILED: Final = "Failed"
|
|
92
|
+
SKIPPED: Final = "Skipped"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class LogLevels:
|
|
96
|
+
"""Log levels of the runner protocol; the worker maps :mod:`logging` levels onto these."""
|
|
97
|
+
|
|
98
|
+
TRACE: Final = "Trace"
|
|
99
|
+
DEBUG: Final = "Debug"
|
|
100
|
+
INFORMATION: Final = "Information"
|
|
101
|
+
WARNING: Final = "Warning"
|
|
102
|
+
ERROR: Final = "Error"
|
|
103
|
+
CRITICAL: Final = "Critical"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class FaultKinds:
|
|
107
|
+
"""How an attempt failed. The kind drives retries."""
|
|
108
|
+
|
|
109
|
+
BUSINESS: Final = "Business"
|
|
110
|
+
APPLICATION: Final = "Application"
|
|
111
|
+
TIMEOUT: Final = "Timeout"
|
|
112
|
+
INFRASTRUCTURE: Final = "Infrastructure"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class FaultCodes:
|
|
116
|
+
"""Well-known fault codes shared by the runner and the Orchestrator."""
|
|
117
|
+
|
|
118
|
+
RUNTIME_NOT_INSTALLED: Final = "RUNTIME_NOT_INSTALLED"
|
|
119
|
+
RUNTIME_DEGRADED: Final = "RUNTIME_DEGRADED"
|
|
120
|
+
PACKAGE_VERIFICATION_FAILED: Final = "PACKAGE_VERIFICATION_FAILED"
|
|
121
|
+
ENVIRONMENT_PREPARATION_FAILED: Final = "ENVIRONMENT_PREPARATION_FAILED"
|
|
122
|
+
EXECUTOR_START_FAILED: Final = "EXECUTOR_START_FAILED"
|
|
123
|
+
EXECUTOR_CRASHED: Final = "EXECUTOR_CRASHED"
|
|
124
|
+
EXECUTOR_PROTOCOL_VIOLATION: Final = "EXECUTOR_PROTOCOL_VIOLATION"
|
|
125
|
+
|
|
126
|
+
#: The start inputs do not fit the handler's signature. Mirrors the workflow executor.
|
|
127
|
+
INVALID_INPUT: Final = "INVALID_INPUT"
|
|
128
|
+
|
|
129
|
+
#: project.json, the entry point or the handler module could not be loaded.
|
|
130
|
+
INVALID_PACKAGE: Final = "INVALID_PACKAGE"
|
|
131
|
+
|
|
132
|
+
#: The handler raised something that is not a BusinessError.
|
|
133
|
+
UNHANDLED_EXCEPTION: Final = "UNHANDLED_EXCEPTION"
|
|
134
|
+
|
|
135
|
+
#: The handler's outputs do not fit in one executor frame (16 MiB by default). Application,
|
|
136
|
+
#: not retryable.
|
|
137
|
+
RESULT_TOO_LARGE: Final = "RESULT_TOO_LARGE"
|
|
138
|
+
|
|
139
|
+
#: The handler returned a value with no JSON form (a ``datetime``, a set, NaN). Application,
|
|
140
|
+
#: not retryable.
|
|
141
|
+
RESULT_NOT_SERIALIZABLE: Final = "RESULT_NOT_SERIALIZABLE"
|
|
142
|
+
|
|
143
|
+
#: A workflow suspended itself and no host is holding its instance. A Python worker never
|
|
144
|
+
#: reports this, but the table is the platform's, so a Python tool that reads a fault off the
|
|
145
|
+
#: Orchestrator must be able to name it.
|
|
146
|
+
WORKFLOW_SUSPENDED: Final = "WORKFLOW_SUSPENDED"
|
|
147
|
+
|
|
148
|
+
#: The executor or the robot was lost after the automation may have acted (crash, lost
|
|
149
|
+
#: lease, forced timeout). Never retried; an operator reconciles. A Python worker never
|
|
150
|
+
#: reports it, but a tool reading a fault off the Orchestrator must be able to name it.
|
|
151
|
+
OUTCOME_UNKNOWN: Final = "VXR-OUTCOME-UNKNOWN"
|
|
152
|
+
|
|
153
|
+
#: An unattended job was refused before launch because no run-as account is configured.
|
|
154
|
+
RUNAS_REQUIRED: Final = "VXR-RUNAS-REQUIRED"
|
|
155
|
+
|
|
156
|
+
#: A workflow package was built under a looser expression policy than the tenant's (engine
|
|
157
|
+
#: ADR 0036). A Python worker never reports it; a tool reading faults must be able to name it.
|
|
158
|
+
EXPRESSION_POLICY_LOOSER: Final = "EXPRESSION_POLICY_LOOSER"
|
|
159
|
+
|
|
160
|
+
# Contracts 0.8.37, agents: the gateway's own refusal codes, carried unchanged into the fault.
|
|
161
|
+
|
|
162
|
+
#: The workspace's AI budget is spent. Application, not retryable.
|
|
163
|
+
AI_BUDGET_EXHAUSTED: Final = "ai_budget_exhausted"
|
|
164
|
+
|
|
165
|
+
#: The workspace's AI policy refused the call. Application, not retryable.
|
|
166
|
+
AI_POLICY_DENIED: Final = "ai_policy_denied"
|
|
167
|
+
|
|
168
|
+
#: The model provider failed after the gateway's retries. Infrastructure.
|
|
169
|
+
AI_PROVIDER_UNAVAILABLE: Final = "ai_provider_unavailable"
|
|
170
|
+
|
|
171
|
+
#: The model call is invalid, e.g. an unknown binding. Application, not retryable.
|
|
172
|
+
AI_REQUEST_INVALID: Final = "ai_request_invalid"
|
|
173
|
+
|
|
174
|
+
#: The answer did not satisfy the output schema after one repair turn. Application, not retryable.
|
|
175
|
+
AGENT_OUTPUT_INVALID: Final = "agent_output_invalid"
|
|
176
|
+
|
|
177
|
+
#: The run reached a guardrail (steps, tokens, timeout). Application, not retryable.
|
|
178
|
+
AGENT_LIMIT_EXCEEDED: Final = "agent_limit_exceeded"
|
|
179
|
+
|
|
180
|
+
#: A tool call ended without a known outcome; reconciled by a person, never replayed.
|
|
181
|
+
TOOL_OUTCOME_UNKNOWN: Final = "tool_outcome_unknown"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class FaultRemediations:
|
|
185
|
+
"""Where the remediation of every ``VXR-*`` fault code is documented (contracts 0.8.9).
|
|
186
|
+
|
|
187
|
+
The remediation table itself lives in the .NET contract; Python only needs the page.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
REFERENCE_PAGE: Final = "VeloPhex.Robot/docs/fault-codes.md"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class AttemptOutcomes:
|
|
194
|
+
"""Terminal outcome of an attempt or a sub-invocation."""
|
|
195
|
+
|
|
196
|
+
SUCCEEDED: Final = "Succeeded"
|
|
197
|
+
FAILED: Final = "Failed"
|
|
198
|
+
CANCELED: Final = "Canceled"
|
|
199
|
+
|
|
200
|
+
#: The run waits and was checkpointed; the job resumes on a later attempt (durable execution,
|
|
201
|
+
#: ADR-P015, Contracts 0.8.22). A Python automation never suspends; the value is mirrored so a
|
|
202
|
+
#: client reading an Orchestrator's attempts knows it.
|
|
203
|
+
SUSPENDED: Final = "Suspended"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ControlSignals:
|
|
207
|
+
"""What the Orchestrator asks a running attempt to do."""
|
|
208
|
+
|
|
209
|
+
NONE: Final = "None"
|
|
210
|
+
|
|
211
|
+
#: Ask the automation to end gracefully.
|
|
212
|
+
STOP: Final = "Stop"
|
|
213
|
+
|
|
214
|
+
#: Terminate the automation immediately.
|
|
215
|
+
KILL: Final = "Kill"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class SignalReasons:
|
|
219
|
+
"""Why a stop was asked for."""
|
|
220
|
+
|
|
221
|
+
USER: Final = "User"
|
|
222
|
+
TIMEOUT: Final = "Timeout"
|
|
223
|
+
|
|
224
|
+
#: ``canceled.reason`` only: the workflow ended itself with Terminate Workflow (contracts
|
|
225
|
+
#: 0.8.11). The run is neither Succeeded nor Failed.
|
|
226
|
+
TERMINATED: Final = "Terminated"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""The states a sign-in passes through, in every client that signs a person in.
|
|
2
|
+
|
|
3
|
+
Mirrors ``VeloPhex.Platform.Contracts.Identity.SignInStates`` (design review 2026-09-23, D1,
|
|
4
|
+
D2, D11). The copy table (``SignInCopy``) stays in the .NET contract for now: the CLI has no
|
|
5
|
+
sign-in of its own yet, and when it gets one it prints these states through the same table
|
|
6
|
+
rather than words of its own.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Final
|
|
12
|
+
|
|
13
|
+
__all__ = ["SignInStates"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SignInStates:
|
|
17
|
+
"""The vocabulary, by value; ``test_vocabulary_mirror`` holds it to the C# source."""
|
|
18
|
+
|
|
19
|
+
SIGNED_OUT: Final = "SignedOut"
|
|
20
|
+
REQUESTING_CODE: Final = "RequestingCode"
|
|
21
|
+
CODE_SHOWN: Final = "CodeShown"
|
|
22
|
+
WAITING_FOR_APPROVAL: Final = "WaitingForApproval"
|
|
23
|
+
APPROVED_COMPLETING: Final = "ApprovedCompleting"
|
|
24
|
+
APPROVED: Final = "Approved"
|
|
25
|
+
EXPIRED: Final = "Expired"
|
|
26
|
+
DENIED: Final = "Denied"
|
|
27
|
+
NO_TENANT: Final = "NoTenant"
|
|
28
|
+
UNREACHABLE: Final = "Unreachable"
|
|
29
|
+
REFUSED: Final = "Refused"
|
|
30
|
+
SESSION_EXPIRED: Final = "SessionExpired"
|
|
31
|
+
|
|
32
|
+
ALL: Final = (
|
|
33
|
+
SIGNED_OUT,
|
|
34
|
+
REQUESTING_CODE,
|
|
35
|
+
CODE_SHOWN,
|
|
36
|
+
WAITING_FOR_APPROVAL,
|
|
37
|
+
APPROVED_COMPLETING,
|
|
38
|
+
APPROVED,
|
|
39
|
+
EXPIRED,
|
|
40
|
+
DENIED,
|
|
41
|
+
NO_TENANT,
|
|
42
|
+
UNREACHABLE,
|
|
43
|
+
REFUSED,
|
|
44
|
+
SESSION_EXPIRED,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
#: States the flow cannot leave by itself: the person acts, or the client starts over.
|
|
48
|
+
TERMINAL: Final = (APPROVED, EXPIRED, DENIED, NO_TENANT, UNREACHABLE, REFUSED, SESSION_EXPIRED)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def is_known(cls, state: str | None) -> bool:
|
|
52
|
+
"""Whether *state* is one of the contract's states (``SignInStates.IsKnown`` in .NET)."""
|
|
53
|
+
return state is not None and state in cls.ALL
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Lexical rules shared with ``VeloPhex.Platform.Contracts.Validation.ContractPatterns``.
|
|
2
|
+
|
|
3
|
+
The patterns are the ones in ``schemas/project.schema.json``; they are repeated here because
|
|
4
|
+
a Python project is validated before it is packed, when no .NET tool is in reach.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MAXIMUM_PACKAGE_ID_LENGTH",
|
|
14
|
+
"MAXIMUM_PATH_LENGTH",
|
|
15
|
+
"MAXIMUM_VERSION_RANGE_LENGTH",
|
|
16
|
+
"has_build_metadata",
|
|
17
|
+
"is_capability",
|
|
18
|
+
"is_entry_point_id",
|
|
19
|
+
"is_package_id",
|
|
20
|
+
"is_python_handler",
|
|
21
|
+
"is_runtime_kind",
|
|
22
|
+
"is_safe_relative_path",
|
|
23
|
+
"is_semantic_version",
|
|
24
|
+
"is_sha512_hex",
|
|
25
|
+
"is_version_range",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
MAXIMUM_PACKAGE_ID_LENGTH: Final = 100
|
|
29
|
+
MAXIMUM_PATH_LENGTH: Final = 260
|
|
30
|
+
MAXIMUM_VERSION_RANGE_LENGTH: Final = 256
|
|
31
|
+
|
|
32
|
+
_PACKAGE_ID = re.compile(r"^\w+([_.-]\w+)*$")
|
|
33
|
+
# SemVer 2.0 without build metadata; byte for byte ContractPatterns.SemanticVersionPattern and
|
|
34
|
+
# project.schema.json's $defs/semanticVersion.
|
|
35
|
+
_SEMANTIC_VERSION = re.compile(
|
|
36
|
+
r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"
|
|
37
|
+
r"(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)"
|
|
38
|
+
r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"
|
|
39
|
+
)
|
|
40
|
+
# The same with a mandatory build-metadata part: what used to be accepted and is now refused with
|
|
41
|
+
# its own diagnostic (VPC1028), so the message can say why.
|
|
42
|
+
_SEMANTIC_VERSION_WITH_BUILD_METADATA = re.compile(
|
|
43
|
+
r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"
|
|
44
|
+
r"(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)"
|
|
45
|
+
r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
|
|
46
|
+
r"\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)$"
|
|
47
|
+
)
|
|
48
|
+
_ENTRY_POINT_ID = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]{0,63}$")
|
|
49
|
+
_PYTHON_HANDLER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*:[A-Za-z_][A-Za-z0-9_]*$")
|
|
50
|
+
_CAPABILITY = re.compile(r"^[a-z][a-z0-9-]{0,31}$")
|
|
51
|
+
_SHA512_HEX = re.compile(r"^[0-9a-f]{128}$")
|
|
52
|
+
_RUNTIME_KIND = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$")
|
|
53
|
+
|
|
54
|
+
# [exact] | (lower?,upper?) | bare, where a version is 1-4 numeric parts with an optional SemVer
|
|
55
|
+
# pre-release and build metadata, and a floating version ends in '*'. Byte for byte the expression
|
|
56
|
+
# in ContractPatterns.VersionRangePattern and in project.schema.json's $defs/versionRange.
|
|
57
|
+
_VERSION_RANGE = re.compile(
|
|
58
|
+
r"^\s*(?:\[\s*[0-9]+(?:\.[0-9]+){0,3}(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?\s*\]"
|
|
59
|
+
r"|[\[(]\s*(?:[0-9]+(?:\.[0-9]+){0,3}(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?"
|
|
60
|
+
r"|\*|[0-9]+(?:\.[0-9]+){0,2}\.\*|[0-9]+(?:\.[0-9]+){0,3}-[0-9A-Za-z.-]*\*)?\s*,\s*"
|
|
61
|
+
r"(?:[0-9]+(?:\.[0-9]+){0,3}(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?)?\s*[\])]"
|
|
62
|
+
r"|[0-9]+(?:\.[0-9]+){0,3}(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?"
|
|
63
|
+
r"|\*|[0-9]+(?:\.[0-9]+){0,2}\.\*|[0-9]+(?:\.[0-9]+){0,3}-[0-9A-Za-z.-]*\*)\s*$"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_FORBIDDEN_CHARACTERS: Final[frozenset[str]] = frozenset('<>"|?*')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_package_id(value: str | None) -> bool:
|
|
70
|
+
"""True for a NuGet-compatible package id, which is also the project name."""
|
|
71
|
+
return bool(value) and len(value or "") <= MAXIMUM_PACKAGE_ID_LENGTH and _PACKAGE_ID.match(value or "") is not None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_semantic_version(value: str | None) -> bool:
|
|
75
|
+
"""True for SemVer 2.0 without a leading ``v`` and without build metadata.
|
|
76
|
+
|
|
77
|
+
``1.0.0`` and ``1.0.0-beta.1``, never ``1.0.0+build.7``: NuGet drops build metadata from a
|
|
78
|
+
package's identity, so a version carrying it would name one artefact in the manifest and
|
|
79
|
+
another on the feed (``ContractPatterns.IsSemanticVersion``; audit PRJ-01).
|
|
80
|
+
"""
|
|
81
|
+
return bool(value) and len(value or "") <= 128 and _SEMANTIC_VERSION.match(value or "") is not None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def has_build_metadata(value: str | None) -> bool:
|
|
85
|
+
"""True for a SemVer 2.0 version that is valid but for its build metadata (``1.0.0+build.7``)."""
|
|
86
|
+
return (
|
|
87
|
+
bool(value) and len(value or "") <= 128 and _SEMANTIC_VERSION_WITH_BUILD_METADATA.match(value or "") is not None
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_entry_point_id(value: str | None) -> bool:
|
|
92
|
+
"""True for an entry point id."""
|
|
93
|
+
return bool(value) and _ENTRY_POINT_ID.match(value or "") is not None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_python_handler(value: str | None) -> bool:
|
|
97
|
+
"""True for a ``module.path:function`` handler (ADR-P002, OD-11)."""
|
|
98
|
+
return bool(value) and len(value or "") <= 256 and _PYTHON_HANDLER.match(value or "") is not None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_capability(value: str | None) -> bool:
|
|
102
|
+
"""True for an execution capability token such as ``network``."""
|
|
103
|
+
return bool(value) and _CAPABILITY.match(value or "") is not None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_sha512_hex(value: str | None) -> bool:
|
|
107
|
+
"""True for a lower-case hex SHA-512."""
|
|
108
|
+
return value is not None and len(value) == 128 and _SHA512_HEX.match(value) is not None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_version_range(value: str | None) -> bool:
|
|
112
|
+
"""True for a NuGet version range, the value side of ``project.json`` -> ``dependencies``.
|
|
113
|
+
|
|
114
|
+
Three spellings: an exact version ``[1.0.0]``, an interval with either bound optional
|
|
115
|
+
(``[1.0.0,2.0.0)``, ``(,2.0.0]``, ``[1.0.0,)``) or a bare version ``1.0.0``, which NuGet reads
|
|
116
|
+
as "that version or higher". Floating versions (``*``, ``1.0.*``, ``1.0.0-*``) are accepted
|
|
117
|
+
where NuGet accepts them: the bare form and the lower bound, never the upper one.
|
|
118
|
+
|
|
119
|
+
Lexical only, like :func:`is_semantic_version`: the SDK must be able to validate a project
|
|
120
|
+
before it is packed, with no NuGet client anywhere in reach.
|
|
121
|
+
"""
|
|
122
|
+
return (
|
|
123
|
+
bool(value)
|
|
124
|
+
and len(value or "") <= MAXIMUM_VERSION_RANGE_LENGTH
|
|
125
|
+
and _VERSION_RANGE.match(value or "") is not None
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def is_runtime_kind(value: str | None) -> bool:
|
|
130
|
+
"""True for a runtime kind token such as ``python``."""
|
|
131
|
+
return bool(value) and len(value or "") <= 32 and _RUNTIME_KIND.match(value or "") is not None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def is_safe_relative_path(value: str | None) -> bool:
|
|
135
|
+
"""True for a relative path that cannot escape its root on any platform.
|
|
136
|
+
|
|
137
|
+
Forward slashes only, no rooted or drive paths, no empty, ``.`` or ``..`` segments, no
|
|
138
|
+
characters Windows forbids and no segment that ends in a dot or a space. These are exactly
|
|
139
|
+
the rules of ``ContractPatterns.IsSafeRelativePath``; the stricter archive rules (length,
|
|
140
|
+
reserved device names) belong to the package builder, which mirrors ``PackagePath``.
|
|
141
|
+
"""
|
|
142
|
+
if not value or len(value) > MAXIMUM_PATH_LENGTH:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
if value.startswith("/") or "\\" in value or ":" in value:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
for segment in value.split("/"):
|
|
149
|
+
if not segment or segment in (".", ".."):
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
if segment.endswith((" ", ".")):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
if any(character in _FORBIDDEN_CHARACTERS or ord(character) < 0x20 for character in segment):
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
return True
|