fdai-deployment-cli 0.1.2__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.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
"""Pure validators for Hub lifecycle configuration packages.
|
|
2
|
+
|
|
3
|
+
The functions in this module validate and resolve candidate configuration bytes before
|
|
4
|
+
signing or import. They do not read files, contact Azure, sign packages, decrypt sealed
|
|
5
|
+
values, grant authority, or establish deployability beyond the returned validation result.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import re
|
|
12
|
+
import unicodedata
|
|
13
|
+
from collections.abc import Container, Mapping
|
|
14
|
+
from collections.abc import Sequence as AbstractSequence
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import cast
|
|
17
|
+
from urllib.parse import urlsplit
|
|
18
|
+
|
|
19
|
+
_ANNOTATION_AXIS = "x-fdai-axis"
|
|
20
|
+
_ANNOTATION_OWNER = "x-fdai-owner"
|
|
21
|
+
_SCHEMA_METADATA = frozenset({_ANNOTATION_AXIS, _ANNOTATION_OWNER, "default"})
|
|
22
|
+
_AUTHORITY_KEYS = frozenset(
|
|
23
|
+
{
|
|
24
|
+
"action-lifecycle",
|
|
25
|
+
"approval-profile",
|
|
26
|
+
"approval-policy",
|
|
27
|
+
"authorization-policy",
|
|
28
|
+
"standing-authority",
|
|
29
|
+
"standing-authorizations",
|
|
30
|
+
"promotion-kind",
|
|
31
|
+
"promotion-state",
|
|
32
|
+
"executor-identity",
|
|
33
|
+
"human-identity",
|
|
34
|
+
"kill-switch",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
_CONFIGURATION_AXES = frozenset(
|
|
38
|
+
{
|
|
39
|
+
"execution-venue",
|
|
40
|
+
"deployment-environment",
|
|
41
|
+
"product-surface-profile",
|
|
42
|
+
"evidence-profile",
|
|
43
|
+
"optional-package-preference",
|
|
44
|
+
"kinetic-evidence-availability",
|
|
45
|
+
"evidence-conflict-state",
|
|
46
|
+
"distribution",
|
|
47
|
+
"operational-safety-profile",
|
|
48
|
+
"installation-path",
|
|
49
|
+
"release-channel-subscription",
|
|
50
|
+
"model-diversity-policy",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
_SEMVER = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$")
|
|
54
|
+
_RANGE_PART = re.compile(r"^(>=|>|<=|<|=)?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$")
|
|
55
|
+
_ACRONYM_BOUNDARY = re.compile(r"(?<=[A-Z])(?=[A-Z][a-z])")
|
|
56
|
+
_CAMEL_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")
|
|
57
|
+
_DIGIT_BOUNDARY = re.compile(r"(?<=[A-Za-z])(?=[0-9])|(?<=[0-9])(?=[A-Za-z])")
|
|
58
|
+
_TOKEN_SPLIT = re.compile(r"[^A-Za-z0-9]+")
|
|
59
|
+
_KEY_VAULT_REF = re.compile(
|
|
60
|
+
r"^kv://[A-Za-z0-9](?:[A-Za-z0-9-]{1,22}[A-Za-z0-9])/[A-Za-z0-9-]{1,127}$"
|
|
61
|
+
)
|
|
62
|
+
_KEY_VAULT_SECRET_REF = re.compile(
|
|
63
|
+
r"^[A-Za-z0-9](?:[A-Za-z0-9-]{1,22}[A-Za-z0-9])/[A-Za-z0-9-]{1,127}$"
|
|
64
|
+
)
|
|
65
|
+
_KEY_VAULT_SECRET_NAME = re.compile(r"^[A-Za-z0-9-]{1,127}$")
|
|
66
|
+
_GUID = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
|
67
|
+
_AZURE_RESOURCE_ID = re.compile(
|
|
68
|
+
r"^/subscriptions/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
69
|
+
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/resourceGroups/[A-Za-z0-9._()-]{1,90}"
|
|
70
|
+
r"(?:/providers/[A-Za-z0-9.]+(?:/[A-Za-z0-9._()-]+/[A-Za-z0-9._()-]+)+)?$"
|
|
71
|
+
)
|
|
72
|
+
_KEY_VAULT_SECRET_ID = re.compile(
|
|
73
|
+
r"^https://[A-Za-z0-9](?:[A-Za-z0-9-]{1,22}[A-Za-z0-9])"
|
|
74
|
+
r"\.vault\.azure\.net/secrets/[A-Za-z0-9-]{1,127}(?:/[0-9a-f]{32})?$"
|
|
75
|
+
)
|
|
76
|
+
_ENUM_VALUE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
|
77
|
+
_LOWER_ENUM_VALUE = re.compile(r"^[a-z][a-z0-9-]{0,31}$")
|
|
78
|
+
_VERSION_VALUE = re.compile(r"^(?:[0-9]{4}-[0-9]{2}-[0-9]{2}|[0-9]+\.[0-9]+(?:\.[0-9]+)?)$")
|
|
79
|
+
_DIRECT_SECRET_TOKENS = frozenset(
|
|
80
|
+
{
|
|
81
|
+
"authorization",
|
|
82
|
+
"credential",
|
|
83
|
+
"credentials",
|
|
84
|
+
"pass",
|
|
85
|
+
"passphrase",
|
|
86
|
+
"passphrases",
|
|
87
|
+
"passwd",
|
|
88
|
+
"password",
|
|
89
|
+
"passwords",
|
|
90
|
+
"pwd",
|
|
91
|
+
"sas",
|
|
92
|
+
"secret",
|
|
93
|
+
"secrets",
|
|
94
|
+
"token",
|
|
95
|
+
"tokens",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
_DIRECT_SECRET_SUBSTRINGS = (
|
|
99
|
+
"apikey",
|
|
100
|
+
"connectionstring",
|
|
101
|
+
"credential",
|
|
102
|
+
"passphrase",
|
|
103
|
+
"passwd",
|
|
104
|
+
"password",
|
|
105
|
+
"privatekey",
|
|
106
|
+
"secret",
|
|
107
|
+
"token",
|
|
108
|
+
)
|
|
109
|
+
_KEY_CONTEXT_TOKENS = frozenset(
|
|
110
|
+
{
|
|
111
|
+
"access",
|
|
112
|
+
"account",
|
|
113
|
+
"api",
|
|
114
|
+
"client",
|
|
115
|
+
"encryption",
|
|
116
|
+
"master",
|
|
117
|
+
"primary",
|
|
118
|
+
"private",
|
|
119
|
+
"secondary",
|
|
120
|
+
"shared",
|
|
121
|
+
"signing",
|
|
122
|
+
"storage",
|
|
123
|
+
"subscription",
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
_REFERENCE_OR_METADATA_FINAL_TOKENS = frozenset(
|
|
127
|
+
{"column", "endpoint", "id", "kind", "name", "ref", "type", "uri", "url", "version"}
|
|
128
|
+
)
|
|
129
|
+
_NUMERIC_COUNTER_TOKENS = frozenset({"count", "limit", "max", "min", "minute", "per", "second"})
|
|
130
|
+
_SENSITIVE_ENUM_VALUES = frozenset(
|
|
131
|
+
{
|
|
132
|
+
"api-key",
|
|
133
|
+
"basic",
|
|
134
|
+
"bearer",
|
|
135
|
+
"certificate",
|
|
136
|
+
"client-credentials",
|
|
137
|
+
"managed-identity",
|
|
138
|
+
"none",
|
|
139
|
+
"oauth2",
|
|
140
|
+
"service-principal",
|
|
141
|
+
"workload-identity",
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
Version = tuple[int, int, int]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ConfigurationValidationError(ValueError):
|
|
149
|
+
"""Configuration validation failure with a stable machine-readable code."""
|
|
150
|
+
|
|
151
|
+
code: str
|
|
152
|
+
path: tuple[str, ...]
|
|
153
|
+
|
|
154
|
+
def __init__(self, code: str, path: AbstractSequence[str], message: str) -> None:
|
|
155
|
+
self.code = code
|
|
156
|
+
self.path = tuple(path)
|
|
157
|
+
super().__init__(message)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True, slots=True)
|
|
161
|
+
class LayerResolution:
|
|
162
|
+
"""Resolved configuration for one Release and Entity."""
|
|
163
|
+
|
|
164
|
+
values: dict[str, object]
|
|
165
|
+
version_range: str
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True, slots=True)
|
|
169
|
+
class _Constraint:
|
|
170
|
+
operator: str
|
|
171
|
+
version: Version
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True, slots=True)
|
|
175
|
+
class _VersionRange:
|
|
176
|
+
source: str
|
|
177
|
+
constraints: tuple[_Constraint, ...]
|
|
178
|
+
lower: Version | None
|
|
179
|
+
lower_inclusive: bool
|
|
180
|
+
upper: Version | None
|
|
181
|
+
upper_inclusive: bool
|
|
182
|
+
|
|
183
|
+
def contains(self, version: Version) -> bool:
|
|
184
|
+
for constraint in self.constraints:
|
|
185
|
+
if constraint.operator == ">=" and version < constraint.version:
|
|
186
|
+
return False
|
|
187
|
+
if constraint.operator == ">" and version <= constraint.version:
|
|
188
|
+
return False
|
|
189
|
+
if constraint.operator == "<=" and version > constraint.version:
|
|
190
|
+
return False
|
|
191
|
+
if constraint.operator == "<" and version >= constraint.version:
|
|
192
|
+
return False
|
|
193
|
+
if constraint.operator == "=" and version != constraint.version:
|
|
194
|
+
return False
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def specificity(self) -> tuple[Version, int, int, Version, int, int]:
|
|
199
|
+
"""Order overlapping ranges by the most constrained matching interval."""
|
|
200
|
+
|
|
201
|
+
lower = self.lower if self.lower is not None else (-1, -1, -1)
|
|
202
|
+
upper = self.upper if self.upper is not None else (0, 0, 0)
|
|
203
|
+
return (
|
|
204
|
+
lower,
|
|
205
|
+
0 if self.lower_inclusive else 1,
|
|
206
|
+
1 if self.upper is not None else 0,
|
|
207
|
+
(-upper[0], -upper[1], -upper[2]),
|
|
208
|
+
1 if not self.upper_inclusive else 0,
|
|
209
|
+
len(self.constraints),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def validate_release_configuration_schema(configuration_schema: Mapping[str, object]) -> None:
|
|
214
|
+
"""Validate Release configuration key annotations without granting authority."""
|
|
215
|
+
|
|
216
|
+
for key, raw_entry in configuration_schema.items():
|
|
217
|
+
_validate_printable_configuration_key(key, (key,))
|
|
218
|
+
entry = _mapping(raw_entry, ("configuration_schema", key))
|
|
219
|
+
axis = entry.get(_ANNOTATION_AXIS)
|
|
220
|
+
owner = entry.get(_ANNOTATION_OWNER)
|
|
221
|
+
if (
|
|
222
|
+
not isinstance(axis, str)
|
|
223
|
+
or not axis.strip()
|
|
224
|
+
or not isinstance(owner, str)
|
|
225
|
+
or not owner.strip()
|
|
226
|
+
):
|
|
227
|
+
raise ConfigurationValidationError(
|
|
228
|
+
"unannotated_configuration_key",
|
|
229
|
+
(key,),
|
|
230
|
+
f"configuration key {key!r} MUST declare x-fdai-axis and x-fdai-owner",
|
|
231
|
+
)
|
|
232
|
+
if _normal_token(axis) not in _CONFIGURATION_AXES:
|
|
233
|
+
raise ConfigurationValidationError(
|
|
234
|
+
"unsupported_configuration_axis",
|
|
235
|
+
(key,),
|
|
236
|
+
f"configuration key {key!r} uses an unsupported or authority axis",
|
|
237
|
+
)
|
|
238
|
+
if owner != "customer":
|
|
239
|
+
raise ConfigurationValidationError(
|
|
240
|
+
"unsupported_configuration_owner",
|
|
241
|
+
(key,),
|
|
242
|
+
"lifecycle configuration keys belong to the customer",
|
|
243
|
+
)
|
|
244
|
+
if set(entry) - _SCHEMA_METADATA:
|
|
245
|
+
raise ConfigurationValidationError(
|
|
246
|
+
"unknown_configuration_metadata",
|
|
247
|
+
(key,),
|
|
248
|
+
"configuration schema metadata is unsupported",
|
|
249
|
+
)
|
|
250
|
+
_reject_authority_keys({key: entry.get("default")}, (key,))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _reject_authority_keys(value: object, path: tuple[str, ...]) -> None:
|
|
254
|
+
if isinstance(value, Mapping):
|
|
255
|
+
for key, item in value.items():
|
|
256
|
+
if isinstance(key, str) and _normal_token(key) in _AUTHORITY_KEYS:
|
|
257
|
+
raise ConfigurationValidationError(
|
|
258
|
+
"authority_configuration_key", path, "authority state is not configuration"
|
|
259
|
+
)
|
|
260
|
+
_reject_authority_keys(item, (*path, str(key)))
|
|
261
|
+
elif isinstance(value, AbstractSequence) and not isinstance(value, str | bytes | bytearray):
|
|
262
|
+
for index, item in enumerate(value):
|
|
263
|
+
_reject_authority_keys(item, (*path, str(index)))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def resolve_configuration_layers(
|
|
267
|
+
*,
|
|
268
|
+
release_version: str,
|
|
269
|
+
configuration_schema: Mapping[str, object],
|
|
270
|
+
environment_config: Mapping[str, object],
|
|
271
|
+
entity_overrides: AbstractSequence[Mapping[str, object]],
|
|
272
|
+
sealed_keys: Container[str] = frozenset(),
|
|
273
|
+
) -> LayerResolution:
|
|
274
|
+
"""Resolve defaults, Environment Config, and the most-specific matching override block."""
|
|
275
|
+
|
|
276
|
+
validate_release_configuration_schema(configuration_schema)
|
|
277
|
+
_reject_authority_keys(environment_config, ("environment_config",))
|
|
278
|
+
version = _parse_version(release_version, ("release_version",))
|
|
279
|
+
values = _defaults(configuration_schema)
|
|
280
|
+
_merge_values(
|
|
281
|
+
values,
|
|
282
|
+
environment_config,
|
|
283
|
+
schema=configuration_schema,
|
|
284
|
+
sealed_keys=sealed_keys,
|
|
285
|
+
path=("environment_config",),
|
|
286
|
+
)
|
|
287
|
+
matching: list[tuple[_VersionRange, Mapping[str, object], int]] = []
|
|
288
|
+
ranges_seen: set[tuple[Version | None, bool, Version | None, bool]] = set()
|
|
289
|
+
for index, raw_block in enumerate(entity_overrides):
|
|
290
|
+
block_path = ("entity_overrides", str(index))
|
|
291
|
+
block = _mapping(raw_block, block_path)
|
|
292
|
+
source = _text(block.get("versions"), (*block_path, "versions"))
|
|
293
|
+
values_raw = _mapping(block.get("values"), (*block_path, "values"))
|
|
294
|
+
_reject_authority_keys(values_raw, (*block_path, "values"))
|
|
295
|
+
version_range = _parse_range(source, (*block_path, "versions"))
|
|
296
|
+
interval = (
|
|
297
|
+
version_range.lower,
|
|
298
|
+
version_range.lower_inclusive,
|
|
299
|
+
version_range.upper,
|
|
300
|
+
version_range.upper_inclusive,
|
|
301
|
+
)
|
|
302
|
+
if interval in ranges_seen:
|
|
303
|
+
raise ConfigurationValidationError(
|
|
304
|
+
"duplicate_override_range",
|
|
305
|
+
(*block_path, "versions"),
|
|
306
|
+
f"Entity override range {source!r} is duplicated",
|
|
307
|
+
)
|
|
308
|
+
ranges_seen.add(interval)
|
|
309
|
+
_validate_values(
|
|
310
|
+
values_raw,
|
|
311
|
+
schema=configuration_schema,
|
|
312
|
+
sealed_keys=sealed_keys,
|
|
313
|
+
path=(*block_path, "values"),
|
|
314
|
+
)
|
|
315
|
+
if version_range.contains(version):
|
|
316
|
+
matching.append((version_range, values_raw, index))
|
|
317
|
+
if not matching:
|
|
318
|
+
raise ConfigurationValidationError(
|
|
319
|
+
"missing_matching_override_block",
|
|
320
|
+
("entity_overrides",),
|
|
321
|
+
f"Release {release_version!r} has no matching Entity override block",
|
|
322
|
+
)
|
|
323
|
+
selected_range, selected_values, _index = max(
|
|
324
|
+
matching, key=lambda item: (item[0].specificity, -item[2])
|
|
325
|
+
)
|
|
326
|
+
if any(not _contains_range(other, selected_range) for other, _values, _i in matching):
|
|
327
|
+
raise ConfigurationValidationError(
|
|
328
|
+
"ambiguous_override_range",
|
|
329
|
+
("entity_overrides",),
|
|
330
|
+
"matching override ranges have no unique most-specific interval",
|
|
331
|
+
)
|
|
332
|
+
_validate_values(
|
|
333
|
+
values, schema=configuration_schema, sealed_keys=sealed_keys, path=("defaults",)
|
|
334
|
+
)
|
|
335
|
+
_merge_values(
|
|
336
|
+
values,
|
|
337
|
+
selected_values,
|
|
338
|
+
schema=configuration_schema,
|
|
339
|
+
sealed_keys=sealed_keys,
|
|
340
|
+
path=("entity_overrides", selected_range.source, "values"),
|
|
341
|
+
)
|
|
342
|
+
return LayerResolution(values=values, version_range=selected_range.source)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _contains_range(outer: _VersionRange, inner: _VersionRange) -> bool:
|
|
346
|
+
if outer.lower is not None and (
|
|
347
|
+
inner.lower is None
|
|
348
|
+
or inner.lower < outer.lower
|
|
349
|
+
or (inner.lower == outer.lower and inner.lower_inclusive and not outer.lower_inclusive)
|
|
350
|
+
):
|
|
351
|
+
return False
|
|
352
|
+
return outer.upper is None or (
|
|
353
|
+
inner.upper is not None
|
|
354
|
+
and (
|
|
355
|
+
inner.upper < outer.upper
|
|
356
|
+
or (inner.upper == outer.upper and (outer.upper_inclusive or not inner.upper_inclusive))
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def validate_configuration_package_for_signing(package: Mapping[str, object]) -> None:
|
|
362
|
+
"""Reject literal secret values before any package signing step can run.
|
|
363
|
+
|
|
364
|
+
Key Vault references are data-plane references and are allowed. Literal values under
|
|
365
|
+
secret-bearing fields fail closed so they cannot be signed into a package.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
_scan_secret_values(package, ())
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _defaults(configuration_schema: Mapping[str, object]) -> dict[str, object]:
|
|
372
|
+
values: dict[str, object] = {}
|
|
373
|
+
for key, raw_entry in configuration_schema.items():
|
|
374
|
+
entry = _mapping(raw_entry, ("configuration_schema", key))
|
|
375
|
+
if "default" in entry:
|
|
376
|
+
values[key] = copy.deepcopy(entry["default"])
|
|
377
|
+
return values
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _merge_values(
|
|
381
|
+
target: dict[str, object],
|
|
382
|
+
update: Mapping[str, object],
|
|
383
|
+
*,
|
|
384
|
+
schema: Mapping[str, object],
|
|
385
|
+
sealed_keys: Container[str],
|
|
386
|
+
path: tuple[str, ...],
|
|
387
|
+
) -> None:
|
|
388
|
+
for key, value in update.items():
|
|
389
|
+
key_path = (*path, key)
|
|
390
|
+
_validate_configuration_key(key, schema=schema, sealed_keys=sealed_keys, path=key_path)
|
|
391
|
+
target[key] = copy.deepcopy(value)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _validate_values(
|
|
395
|
+
values: Mapping[str, object],
|
|
396
|
+
*,
|
|
397
|
+
schema: Mapping[str, object],
|
|
398
|
+
sealed_keys: Container[str],
|
|
399
|
+
path: tuple[str, ...],
|
|
400
|
+
) -> None:
|
|
401
|
+
for key in values:
|
|
402
|
+
_validate_configuration_key(key, schema=schema, sealed_keys=sealed_keys, path=(*path, key))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _validate_configuration_key(
|
|
406
|
+
key: str,
|
|
407
|
+
*,
|
|
408
|
+
schema: Mapping[str, object],
|
|
409
|
+
sealed_keys: Container[str],
|
|
410
|
+
path: tuple[str, ...],
|
|
411
|
+
) -> None:
|
|
412
|
+
_validate_printable_configuration_key(key, path)
|
|
413
|
+
if key not in schema:
|
|
414
|
+
raise ConfigurationValidationError(
|
|
415
|
+
"unknown_configuration_key",
|
|
416
|
+
path,
|
|
417
|
+
f"configuration key {key!r} is not declared by the Release schema",
|
|
418
|
+
)
|
|
419
|
+
if key in sealed_keys:
|
|
420
|
+
raise ConfigurationValidationError(
|
|
421
|
+
"sealed_configuration_key",
|
|
422
|
+
path,
|
|
423
|
+
f"configuration key {key!r} belongs in the sealed section",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _scan_secret_values(value: object, path: tuple[str, ...]) -> None:
|
|
428
|
+
if isinstance(value, Mapping):
|
|
429
|
+
_scan_name_value_secret(value, path)
|
|
430
|
+
for raw_key, raw_item in value.items():
|
|
431
|
+
key = str(raw_key)
|
|
432
|
+
item_path = (*path, key)
|
|
433
|
+
_validate_printable_configuration_key(key, item_path)
|
|
434
|
+
if _is_sensitive_key(key):
|
|
435
|
+
_validate_secret_reference(raw_item, item_path, key)
|
|
436
|
+
continue
|
|
437
|
+
_scan_secret_values(raw_item, item_path)
|
|
438
|
+
return
|
|
439
|
+
if isinstance(value, AbstractSequence) and not isinstance(value, str | bytes | bytearray):
|
|
440
|
+
for index, item in enumerate(value):
|
|
441
|
+
_scan_secret_values(item, (*path, str(index)))
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _scan_name_value_secret(value: Mapping[object, object], path: tuple[str, ...]) -> None:
|
|
445
|
+
name_keys = [*_find_casefold_keys(value, "name"), *_find_casefold_keys(value, "key")]
|
|
446
|
+
value_keys = _find_casefold_keys(value, "value")
|
|
447
|
+
if not name_keys or not value_keys:
|
|
448
|
+
return
|
|
449
|
+
for name_key in name_keys:
|
|
450
|
+
name = value[name_key]
|
|
451
|
+
if not isinstance(name, str) or not _is_sensitive_key(name):
|
|
452
|
+
continue
|
|
453
|
+
for value_key in value_keys:
|
|
454
|
+
_validate_secret_reference(value[value_key], (*path, str(value_key)), name)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _validate_secret_reference(value: object, path: tuple[str, ...], key: str) -> None:
|
|
458
|
+
tokens = _tokens(key)
|
|
459
|
+
if isinstance(value, bool):
|
|
460
|
+
return
|
|
461
|
+
if isinstance(value, str) and _is_reference_or_metadata_key(tokens):
|
|
462
|
+
_validate_reference_or_metadata_value(tokens, value, path, key)
|
|
463
|
+
return
|
|
464
|
+
if _is_numeric_token_counter(tokens, value):
|
|
465
|
+
return
|
|
466
|
+
if _is_ref_key(tokens) and isinstance(value, str) and _KEY_VAULT_SECRET_NAME.fullmatch(value):
|
|
467
|
+
return
|
|
468
|
+
if _is_key_vault_reference(value):
|
|
469
|
+
return
|
|
470
|
+
_raise_literal_secret(path, key)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _is_valid_ref_value(value: str) -> bool:
|
|
474
|
+
return _KEY_VAULT_SECRET_NAME.fullmatch(value) is not None or _is_key_vault_reference(value)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _validate_reference_or_metadata_value(
|
|
478
|
+
tokens: tuple[str, ...], value: str, path: tuple[str, ...], key: str
|
|
479
|
+
) -> None:
|
|
480
|
+
final = tokens[-1]
|
|
481
|
+
prefix_sensitive = _metadata_prefix_is_sensitive(tokens)
|
|
482
|
+
if final == "ref":
|
|
483
|
+
if _is_valid_ref_value(value):
|
|
484
|
+
return
|
|
485
|
+
_raise_literal_secret(path, key)
|
|
486
|
+
if final == "name":
|
|
487
|
+
# The lifecycle design uses Key Vault secret names as references for secret_name and
|
|
488
|
+
# key_vault_secret_name. Other sensitive *_name fields can hide real literal secrets.
|
|
489
|
+
if (
|
|
490
|
+
tokens[:-1]
|
|
491
|
+
and (not prefix_sensitive or _is_allowed_name_reference_key(tokens))
|
|
492
|
+
and _KEY_VAULT_SECRET_NAME.fullmatch(value) is not None
|
|
493
|
+
):
|
|
494
|
+
return
|
|
495
|
+
_raise_literal_secret(path, key)
|
|
496
|
+
if final in {"url", "uri", "endpoint"}:
|
|
497
|
+
if _is_safe_https_url(value):
|
|
498
|
+
return
|
|
499
|
+
_raise_literal_secret(path, key)
|
|
500
|
+
if final == "id":
|
|
501
|
+
if _KEY_VAULT_SECRET_ID.fullmatch(value) is not None:
|
|
502
|
+
return
|
|
503
|
+
if not prefix_sensitive and (
|
|
504
|
+
_GUID.fullmatch(value) is not None or _AZURE_RESOURCE_ID.fullmatch(value) is not None
|
|
505
|
+
):
|
|
506
|
+
return
|
|
507
|
+
_raise_literal_secret(path, key)
|
|
508
|
+
if final in {"kind", "type", "version"}:
|
|
509
|
+
if _is_valid_enum_metadata(tokens, value):
|
|
510
|
+
return
|
|
511
|
+
_raise_literal_secret(path, key)
|
|
512
|
+
if final == "column":
|
|
513
|
+
if not prefix_sensitive and _ENUM_VALUE.fullmatch(value) is not None:
|
|
514
|
+
return
|
|
515
|
+
_raise_literal_secret(path, key)
|
|
516
|
+
_raise_literal_secret(path, key)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _is_allowed_name_reference_key(tokens: tuple[str, ...]) -> bool:
|
|
520
|
+
return tokens in {("secret", "name"), ("key", "vault", "secret", "name")}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _metadata_prefix_is_sensitive(tokens: tuple[str, ...]) -> bool:
|
|
524
|
+
if len(tokens) <= 1:
|
|
525
|
+
return False
|
|
526
|
+
prefix = tokens[:-1]
|
|
527
|
+
return _is_sensitive_key("_".join(prefix)) or _is_sensitive_key("".join(prefix))
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _is_contextual_key_secret(tokens: tuple[str, ...]) -> bool:
|
|
531
|
+
token_set = set(tokens)
|
|
532
|
+
return bool(token_set & {"key", "keys"} and token_set & _KEY_CONTEXT_TOKENS)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _is_valid_enum_metadata(tokens: tuple[str, ...], value: str) -> bool:
|
|
536
|
+
if _metadata_prefix_is_sensitive(tokens) or _is_contextual_key_secret(tokens):
|
|
537
|
+
if tokens[-1] == "version" and _VERSION_VALUE.fullmatch(value) is not None:
|
|
538
|
+
return True
|
|
539
|
+
normalized = value.casefold()
|
|
540
|
+
return (
|
|
541
|
+
normalized in _SENSITIVE_ENUM_VALUES
|
|
542
|
+
and _LOWER_ENUM_VALUE.fullmatch(normalized) is not None
|
|
543
|
+
)
|
|
544
|
+
if tokens[-1] == "version" and _VERSION_VALUE.fullmatch(value) is not None:
|
|
545
|
+
return True
|
|
546
|
+
return _ENUM_VALUE.fullmatch(value) is not None and not _looks_random_enum_value(value)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _looks_random_enum_value(value: str) -> bool:
|
|
550
|
+
if len(value) < 16:
|
|
551
|
+
return False
|
|
552
|
+
digit_ratio = sum(char.isdigit() for char in value) / len(value)
|
|
553
|
+
if digit_ratio > 0.35:
|
|
554
|
+
return True
|
|
555
|
+
unique_ratio = len(set(value.casefold())) / len(value)
|
|
556
|
+
return unique_ratio > 0.70
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _is_safe_https_url(value: str) -> bool:
|
|
560
|
+
parsed = urlsplit(value)
|
|
561
|
+
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
|
|
562
|
+
return False
|
|
563
|
+
return not (parsed.query or parsed.fragment)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _is_key_vault_reference(value: object) -> bool:
|
|
567
|
+
if isinstance(value, str) and _KEY_VAULT_REF.fullmatch(value) is not None:
|
|
568
|
+
return True
|
|
569
|
+
return (
|
|
570
|
+
isinstance(value, Mapping)
|
|
571
|
+
and set(value) == {"key_vault_secret"}
|
|
572
|
+
and isinstance(value["key_vault_secret"], str)
|
|
573
|
+
and _KEY_VAULT_SECRET_REF.fullmatch(value["key_vault_secret"]) is not None
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _raise_literal_secret(path: tuple[str, ...], key: str) -> None:
|
|
578
|
+
raise ConfigurationValidationError(
|
|
579
|
+
"literal_secret_value",
|
|
580
|
+
path,
|
|
581
|
+
f"field {key!r} contains a literal secret value or malformed secret reference",
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _find_casefold_keys(value: Mapping[object, object], target: str) -> tuple[object, ...]:
|
|
586
|
+
matches: list[object] = []
|
|
587
|
+
for key in value:
|
|
588
|
+
if isinstance(key, str) and key.casefold() == target:
|
|
589
|
+
matches.append(key)
|
|
590
|
+
return tuple(matches)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _mapping(value: object, path: tuple[str, ...]) -> Mapping[str, object]:
|
|
594
|
+
if not isinstance(value, Mapping):
|
|
595
|
+
raise ConfigurationValidationError(
|
|
596
|
+
"invalid_mapping",
|
|
597
|
+
path,
|
|
598
|
+
f"{'.'.join(path) or 'configuration'} MUST be a mapping",
|
|
599
|
+
)
|
|
600
|
+
return cast(Mapping[str, object], value)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _text(value: object, path: tuple[str, ...]) -> str:
|
|
604
|
+
if not isinstance(value, str) or not value:
|
|
605
|
+
raise ConfigurationValidationError(
|
|
606
|
+
"invalid_string",
|
|
607
|
+
path,
|
|
608
|
+
f"{'.'.join(path)} MUST be a non-empty string",
|
|
609
|
+
)
|
|
610
|
+
return value
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _normal_token(value: str) -> str:
|
|
614
|
+
tokens = _tokens(value)
|
|
615
|
+
return "-".join(tokens)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _is_sensitive_key(value: str) -> bool:
|
|
619
|
+
tokens_tuple = _tokens(value)
|
|
620
|
+
tokens = set(tokens_tuple)
|
|
621
|
+
if tokens & _DIRECT_SECRET_TOKENS:
|
|
622
|
+
return True
|
|
623
|
+
if any(marker in token for token in tokens_tuple for marker in _DIRECT_SECRET_SUBSTRINGS):
|
|
624
|
+
return True
|
|
625
|
+
if "connection" in tokens and "string" in tokens:
|
|
626
|
+
return True
|
|
627
|
+
return bool(tokens & {"key", "keys"} and tokens & _KEY_CONTEXT_TOKENS)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _is_ref_key(tokens: tuple[str, ...]) -> bool:
|
|
631
|
+
return bool(tokens) and tokens[-1] == "ref"
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _is_reference_or_metadata_key(tokens: tuple[str, ...]) -> bool:
|
|
635
|
+
return bool(tokens) and tokens[-1] in _REFERENCE_OR_METADATA_FINAL_TOKENS
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _is_numeric_token_counter(tokens: tuple[str, ...], value: object) -> bool:
|
|
639
|
+
if isinstance(value, bool) or not isinstance(value, int | float):
|
|
640
|
+
return False
|
|
641
|
+
token_set = set(tokens)
|
|
642
|
+
return bool(token_set & {"token", "tokens"} and token_set & _NUMERIC_COUNTER_TOKENS)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _tokens(value: str) -> tuple[str, ...]:
|
|
646
|
+
separated = _ACRONYM_BOUNDARY.sub("-", value)
|
|
647
|
+
separated = _CAMEL_BOUNDARY.sub("-", separated)
|
|
648
|
+
separated = _DIGIT_BOUNDARY.sub("-", separated)
|
|
649
|
+
return tuple(token.lower() for token in _TOKEN_SPLIT.split(separated) if token)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _validate_printable_configuration_key(key: str, path: tuple[str, ...]) -> None:
|
|
653
|
+
normalized = unicodedata.normalize("NFKC", key)
|
|
654
|
+
if normalized != key or any(unicodedata.category(char) == "Cf" for char in key):
|
|
655
|
+
raise ConfigurationValidationError(
|
|
656
|
+
"invalid_configuration_key",
|
|
657
|
+
path,
|
|
658
|
+
"configuration keys MUST be printable ASCII",
|
|
659
|
+
)
|
|
660
|
+
if any(ord(char) < 0x20 or ord(char) > 0x7E for char in normalized):
|
|
661
|
+
raise ConfigurationValidationError(
|
|
662
|
+
"invalid_configuration_key",
|
|
663
|
+
path,
|
|
664
|
+
"configuration keys MUST be printable ASCII",
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _parse_version(value: str, path: tuple[str, ...]) -> Version:
|
|
669
|
+
match = _SEMVER.fullmatch(value)
|
|
670
|
+
if match is None:
|
|
671
|
+
raise ConfigurationValidationError(
|
|
672
|
+
"invalid_release_version",
|
|
673
|
+
path,
|
|
674
|
+
f"{'.'.join(path)} MUST be a semantic version",
|
|
675
|
+
)
|
|
676
|
+
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _parse_range(value: str, path: tuple[str, ...]) -> _VersionRange:
|
|
680
|
+
constraints: list[_Constraint] = []
|
|
681
|
+
lower: Version | None = None
|
|
682
|
+
lower_inclusive = True
|
|
683
|
+
upper: Version | None = None
|
|
684
|
+
upper_inclusive = True
|
|
685
|
+
for part in value.split():
|
|
686
|
+
match = _RANGE_PART.fullmatch(part)
|
|
687
|
+
if match is None:
|
|
688
|
+
raise ConfigurationValidationError(
|
|
689
|
+
"invalid_version_range",
|
|
690
|
+
path,
|
|
691
|
+
f"version range {value!r} is invalid",
|
|
692
|
+
)
|
|
693
|
+
operator = match.group(1) or "="
|
|
694
|
+
version = (int(match.group(2)), int(match.group(3)), int(match.group(4)))
|
|
695
|
+
constraints.append(_Constraint(operator=operator, version=version))
|
|
696
|
+
if operator in {">=", ">"}:
|
|
697
|
+
if lower is None or version > lower or (version == lower and operator == ">"):
|
|
698
|
+
lower = version
|
|
699
|
+
lower_inclusive = operator == ">="
|
|
700
|
+
elif operator in {"<=", "<"}:
|
|
701
|
+
if upper is None or version < upper or (version == upper and operator == "<"):
|
|
702
|
+
upper = version
|
|
703
|
+
upper_inclusive = operator == "<="
|
|
704
|
+
else:
|
|
705
|
+
lower = version
|
|
706
|
+
upper = version
|
|
707
|
+
lower_inclusive = True
|
|
708
|
+
upper_inclusive = True
|
|
709
|
+
if not constraints:
|
|
710
|
+
raise ConfigurationValidationError(
|
|
711
|
+
"invalid_version_range",
|
|
712
|
+
path,
|
|
713
|
+
"version range MUST contain at least one constraint",
|
|
714
|
+
)
|
|
715
|
+
return _VersionRange(
|
|
716
|
+
source=value,
|
|
717
|
+
constraints=tuple(constraints),
|
|
718
|
+
lower=lower,
|
|
719
|
+
lower_inclusive=lower_inclusive,
|
|
720
|
+
upper=upper,
|
|
721
|
+
upper_inclusive=upper_inclusive,
|
|
722
|
+
)
|