Skip to content
Merged
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`Invalid SQL identifier: 'spectrum_events\n'`. That includes a name that reaches it with a
trailing newline from an environment variable or a file; strip the value where it is set.

- **Other validators let a value with a trailing newline through, too.** The same `$` ended other
whole-string patterns, so a value written as a YAML `|` block, read from a `.env` file or decoded
from `%0A` in a URL kept its newline: `fluid generate iac` put `region = "eu-west-1\n"` on the
AWS provider, and a Databricks job got `branch = "main\n"` from `?branch=main%0A`. They now end
in `\Z` or are matched with `fullmatch`, so such a value is refused with the validator's usual
message, or takes the path the code already has for a value that is not a plain name: a product
id forged from a JDBC source or imported from dbt or ODCS is slugified. An AWS binding region
with whitespace around it is refused (`aws-region-whitespace`), because the sovereignty policy
checks the region as written. Covered: quality-check selectors, catalog and JDBC identifiers,
BigQuery and GCP project ids, contract, build, federation-workspace and overlay-env ids,
row-filter names, Singer tap and target names, prompt profile and overlay names, mission
predicate paths, AWS and GCP resource names, Snowflake account segments, Databricks git refs,
and the identifier and version checks of ODCS and ODPS imports.

## [0.26.0] — 2026-11-01

### Added
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/build_runners/_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
# DOS-via-filename attacks. Crucially this excludes ``/``, ``\``, ``..``
# (a leading dot is rejected) and any path separator, so a validated id
# can never traverse out of the directory it is joined into.
_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.\-]{0,127}$")
_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.\-]{0,127}\Z")


class IdentifierViolation(ValueError):
Expand Down
4 changes: 2 additions & 2 deletions fluid_build/build_runners/meltano/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ def collect_singer_output(
# ── Tap invocation ──────────────────────────────────────────────────────


_TAP_NAME_RE = re.compile(r"^tap-[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$")
_TARGET_NAME_RE = re.compile(r"^target-[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$")
_TAP_NAME_RE = re.compile(r"^tap-[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\Z")
_TARGET_NAME_RE = re.compile(r"^target-[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\Z")


def _resolve_tap_binary(tap_name: str, *, project_dir: Optional[Path] = None) -> Optional[str]:
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/_ai_setup_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def _plaintext_env_var() -> str:
# Privacy guard: only a bare domain slug is ever persisted. Mirrors
# ``forge_domain_enrichment._SAFE_DOMAIN_RE``, lower-cased and length-capped —
# the regex is what keeps prompt text / paths / PII out of the history file.
_DOMAIN_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{0,63}$")
_DOMAIN_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{0,63}\Z")


def _allow_plaintext_ai_secrets() -> bool:
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/_forge_data_model_jdbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

# Canonical FLUID contract-id pattern (lowercased form — ``product_id`` is
# lowercased before the check). Mirrors the JSON-schema id constraint.
_FLUID_ID_RE = re.compile(r"^[a-z0-9_][a-z0-9_.-]*[a-z0-9_]$|^[a-z0-9_]$")
_FLUID_ID_RE = re.compile(r"^[a-z0-9_][a-z0-9_.-]*[a-z0-9_]\Z|^[a-z0-9_]\Z")


def _run_from_jdbc_source(
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/discover/_jdbc_introspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ def _attach_string_sqlite(args: Dict[str, str], alias: str) -> str:
return f"ATTACH {quote_ansi_string_literal(path)} AS {alias} (TYPE sqlite)"


_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*\Z")


def _validate_alias(alias: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/forge_copilot_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def get_active_domain() -> Optional[str]:
# leading dot/underscore. This is the first line of defence against
# ``--prompt-profile ../../etc``; ``_resolve_profile_dir`` adds a resolved-path
# containment check as the second.
_PROFILE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
_PROFILE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*\Z")


class PromptProfileError(ValueError):
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/forge_domain_enrichment.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
# override — defence in depth against a ``../``-style traversal reaching a
# fragment block in an out-of-tree YAML. Matches the profile-name guard in
# ``forge_copilot_prompts._PROFILE_NAME_RE``.
_SAFE_DOMAIN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
_SAFE_DOMAIN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*\Z")


@lru_cache(maxsize=1)
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/forge_prompt_overlays.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
# leading dot/underscore. First line of defence against
# ``--prompt-overlay ../../etc``; :func:`_resolve_overlay_path` adds a
# resolved-realpath containment check as the second.
_OVERLAY_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
_OVERLAY_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*\Z")

_ALLOWED_MODES = ("replace", "append", "prepend")

Expand Down
4 changes: 2 additions & 2 deletions fluid_build/cli/forge_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def validate_project_name(name: str) -> Tuple[bool, Optional[str]]:
if len(name) > 63:
return False, "Project name must be 63 characters or less"

if not re.match(r"^[a-z][a-z0-9-]*[a-z0-9]$", name):
if not re.match(r"^[a-z][a-z0-9-]*[a-z0-9]\Z", name):
return (
False,
"Project name must start with a letter, end with letter/number, and contain only lowercase letters, numbers, and hyphens",
Expand Down Expand Up @@ -147,7 +147,7 @@ def validate_template_name(template: str) -> Tuple[bool, Optional[str]]:
if len(template) < 2:
return False, "Template name must be at least 2 characters"

if not re.match(r"^[a-z][a-z0-9-]*$", template):
if not re.match(r"^[a-z][a-z0-9-]*\Z", template):
return (
False,
"Template name must start with a letter and contain only lowercase letters, numbers, and hyphens",
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/import_workflow/dbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
_REF_RE = re.compile(r"ref\(\s*(?:['\"][^'\"]+['\"]\s*,\s*)?['\"]([^'\"]+)['\"]\s*\)")

# FLUID identifier pattern (schema `$defs.identifier`).
_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]*[A-Za-z0-9_]$|^[A-Za-z0-9_]$")
_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]*[A-Za-z0-9_]\Z|^[A-Za-z0-9_]\Z")

# dbt folder layer → medallion layer (mirrors engines/dbt/models.py).
_LAYER_TO_MEDALLION = {"staging": "Bronze", "intermediate": "Silver", "marts": "Gold"}
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ def validate_project_name(name: str) -> bool:
return False

# Check format: alphanumeric, hyphens, underscores only
if not re.match(r"^[a-zA-Z0-9_-]+$", name):
if not re.match(r"^[a-zA-Z0-9_-]+\Z", name):
return False

# Check length
Expand Down
5 changes: 3 additions & 2 deletions fluid_build/copilot/catalog/_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@

# Conservative default identifier shape — covers Snowflake / BigQuery /
# Unity / Glue without quoting weirdness. Catalogs with looser rules
# (e.g., DataHub URN strings) override the regex per-adapter.
# (e.g., DataHub URN strings) override the regex per-adapter. Matched
# with ``fullmatch``: ``$`` also matches before a trailing newline.
_DEFAULT_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


Expand Down Expand Up @@ -143,7 +144,7 @@ def validate_and_quote_identifier(
non-SQL-shaped identifiers.
"""
rule = pattern or _DEFAULT_IDENTIFIER
if not rule.match(value or ""):
if not rule.fullmatch(value or ""):
raise CatalogConfigError(
message=f"Invalid {kind} identifier: {value!r}",
suggestions=[
Expand Down
4 changes: 3 additions & 1 deletion fluid_build/copilot/catalog/bigquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
#
# The pattern table here keeps both rules visible — adding a new
# identifier kind (e.g., a connection name) just adds one entry.
# Match these with ``fullmatch``: their ``$`` also matches before a
# trailing newline, so ``.match`` lets ``"orders\n"`` through.
_BQ_IDENT_PATTERNS = {
# Allow hyphens for project IDs.
"project": re.compile(r"^[A-Za-z][A-Za-z0-9-]*$"),
Expand Down Expand Up @@ -113,7 +115,7 @@ def _quote_ident(value: str, *, kind: str) -> str:
def _validate_ident(value: str, *, kind: str) -> str:
"""Return a BigQuery identifier after strict kind-specific validation."""
pattern = _BQ_IDENT_PATTERNS.get(kind)
if pattern is None or not pattern.match(value or ""):
if pattern is None or not pattern.fullmatch(value or ""):
raise CatalogConfigError(
message=f"Invalid BigQuery {kind} identifier: {value!r}",
suggestions=[
Expand Down
4 changes: 2 additions & 2 deletions fluid_build/copilot/missions/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
_BUDGET_KEYS = frozenset({"max_usd", "max_iterations", "max_wall_seconds"})

#: One dotted-path segment: a key, optionally fanned out over an array.
_SEGMENT_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_-]*)(\[\*\])?$")
_SEGMENT_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_-]*)(\[\*\])?\Z")

_MISSION_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")

Expand Down Expand Up @@ -172,7 +172,7 @@ def validate_predicate_path(path: str, *, label: str) -> None:
for segment in path.split("."):
if not _SEGMENT_RE.match(segment):
raise MissionSpecError(
f"{label}: invalid path segment '{segment}' in '{path}'. The predicate "
f"{label}: invalid path segment {segment!r} in {path!r}. The predicate "
"grammar is frozen: dotted keys with optional [*] array fan-out "
"(e.g. exposes[*].contract.dq.rules)."
)
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/forge/core/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ def bundle_overlay_warnings(source: Optional[Mapping[str, Any]]) -> List[str]:
#: path. It selects the overlay file whose ``id`` the recorded source
#: contract is checked with, so a value like ``../../x`` must not be able to
#: pick an arbitrary file for that check.
_ENV_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
_ENV_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*\Z")


def bundle_source_contract(
Expand Down
3 changes: 2 additions & 1 deletion fluid_build/forge/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ def _federation_git_timeout() -> float:
# (``~/.cache/fluid/federation-git/<id>`` and the digest-cache file).
# Constrain it to a conservative slug so a tampered manifest row can't
# smuggle ``../`` traversal or absolute-path segments into those sinks.
# Matched with ``fullmatch``: ``$`` also matches before a trailing newline.
_WORKSPACE_ID_RE = re.compile(r"^[A-Za-z0-9_.-]{1,64}$")


Expand Down Expand Up @@ -589,7 +590,7 @@ def from_dict(cls, d: Mapping[str, Any]) -> "FederatedWorkspace":
# anything outside a conservative slug so a tampered manifest
# row cannot smuggle ``../`` traversal or absolute segments into
# those path sinks.
if not _WORKSPACE_ID_RE.match(ws_id):
if not _WORKSPACE_ID_RE.fullmatch(ws_id):
raise ValueError(
f"federation workspace id {ws_id!r} is invalid: must match "
f"{_WORKSPACE_ID_RE.pattern} (alphanumerics, dot, dash, "
Expand Down
25 changes: 24 additions & 1 deletion fluid_build/iac/providers/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
# binding carries (a jurisdiction such as "EU", a Google region, an unresolved
# "{{ env.AWS_REGION }}") must not become the provider's region; it used to be
# ignored, and still is.
_AWS_REGION_RE = re.compile(r"^[a-z]{2}(-[a-z]+)+-\d+$")
_AWS_REGION_RE = re.compile(r"^[a-z]{2}(-[a-z]+)+-\d+\Z")


def recorded_regions(resources: Iterable[Mapping[str, Any]]) -> set:
Expand Down Expand Up @@ -735,6 +735,29 @@ def provider_block_for(self, contract: Mapping[str, Any]) -> Dict[str, Any]:
provider v6).
"""
cfg = dict(self.provider_block())
for index, expose in enumerate(contract.get("exposes") or []):
binding = expose.get("binding") or {}
raw = (binding.get("location") or {}).get("region")
# A region code with whitespace around it (a YAML ``|`` block ends in a
# newline) is refused. Dropping it leaves the provider on the
# environment's region; pinning it stripped applies a region the
# sovereignty policy, which reads the value as written, never checked.
if (
is_cloud(binding, "aws")
and isinstance(raw, str)
and raw != raw.strip()
and _AWS_REGION_RE.match(raw.strip())
):
raise UnsupportedBindingError(
"aws-region-whitespace",
f"exposes[{expose.get('exposeId') or index}] binding.location.region is "
f"{raw!r}: a region code with whitespace around it. The sovereignty policy "
"checks the region as written, so it is refused rather than pinned stripped.",
(
f"Write the region alone: region: {raw.strip()} (a YAML '|' block ends "
"the value in a newline).",
),
)
named = {
str(((e.get("binding") or {}).get("location") or {}).get("region"))
for e in contract.get("exposes") or []
Expand Down
6 changes: 3 additions & 3 deletions fluid_build/iac/providers/databricks_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ def _under_home(prefix: str, suffix: str) -> TofuExpr:
"dev.azure.com": "azureDevOpsServices",
}
_GIT_REFS = ("branch", "tag", "commit")
_GIT_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*$")
_COMMIT_RE = re.compile(r"^[0-9a-fA-F]{7,40}$")
_REPO_PATH_RE = re.compile(r"^[A-Za-z0-9._/-]+$")
_GIT_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*\Z")
_COMMIT_RE = re.compile(r"^[0-9a-fA-F]{7,40}\Z")
_REPO_PATH_RE = re.compile(r"^[A-Za-z0-9._/-]+\Z")

#: ``1h``, ``30m``, ``45s``, ``2d``, a bare number of seconds, or ISO 8601 ``PT1H30M``.
_DURATION_RE = re.compile(r"^(\d+)\s*([smhd]?)$")
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/iac/row_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from .base import UnsupportedBindingError
from .column_access import policy_where

_NAME = re.compile(r"^[a-z][a-z0-9_]{0,62}$")
_NAME = re.compile(r"^[a-z][a-z0-9_]{0,62}\Z")


@dataclass(frozen=True)
Expand Down
16 changes: 8 additions & 8 deletions fluid_build/providers/aws/util/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ class ResourceValidator:
"""Validates AWS resource configurations before execution."""

# AWS naming rules
S3_BUCKET_PATTERN = re.compile(r"^[a-z0-9][a-z0-9\-]{1,61}[a-z0-9]$")
GLUE_DATABASE_PATTERN = re.compile(r"^[a-z0-9_]+$")
GLUE_TABLE_PATTERN = re.compile(r"^[a-z0-9_]+$")
LAMBDA_FUNCTION_PATTERN = re.compile(r"^[a-zA-Z0-9\-_]+$")
S3_BUCKET_PATTERN = re.compile(r"^[a-z0-9][a-z0-9\-]{1,61}[a-z0-9]\Z")
GLUE_DATABASE_PATTERN = re.compile(r"^[a-z0-9_]+\Z")
GLUE_TABLE_PATTERN = re.compile(r"^[a-z0-9_]+\Z")
LAMBDA_FUNCTION_PATTERN = re.compile(r"^[a-zA-Z0-9\-_]+\Z")

# AWS service quotas (defaults - can be increased)
DEFAULT_QUOTAS = {
Expand Down Expand Up @@ -91,7 +91,7 @@ def validate_s3_bucket_name(self, bucket: str) -> Tuple[bool, Optional[str]]:

if not self.S3_BUCKET_PATTERN.match(bucket):
return False, (
f"Bucket name '{bucket}' is invalid. "
f"Bucket name {bucket!r} is invalid. "
"Must contain only lowercase letters, numbers, and hyphens. "
"Must start and end with letter or number."
)
Expand Down Expand Up @@ -135,7 +135,7 @@ def validate_glue_database_name(self, database: str) -> Tuple[bool, Optional[str

if not self.GLUE_DATABASE_PATTERN.match(database):
return False, (
f"Database name '{database}' is invalid. "
f"Database name {database!r} is invalid. "
"Must contain only lowercase letters, numbers, and underscores."
)

Expand Down Expand Up @@ -164,7 +164,7 @@ def validate_glue_table_name(self, table: str) -> Tuple[bool, Optional[str]]:

if not self.GLUE_TABLE_PATTERN.match(table):
return False, (
f"Table name '{table}' is invalid. "
f"Table name {table!r} is invalid. "
"Must contain only lowercase letters, numbers, and underscores."
)

Expand Down Expand Up @@ -193,7 +193,7 @@ def validate_lambda_function_name(self, function: str) -> Tuple[bool, Optional[s

if not self.LAMBDA_FUNCTION_PATTERN.match(function):
return False, (
f"Function name '{function}' is invalid. "
f"Function name {function!r} is invalid. "
"Must contain only letters, numbers, hyphens, and underscores."
)

Expand Down
2 changes: 1 addition & 1 deletion fluid_build/providers/bigquery_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
# need a dedicated allowlist: 6–30 chars, lowercase-or-mixed letter start,
# letters/digits/hyphens in the middle, and a letter/digit terminator.
# See https://cloud.google.com/resource-manager/docs/creating-managing-projects.
_BQ_PROJECT_ID = re.compile(r"^[A-Za-z][A-Za-z0-9-]{4,28}[A-Za-z0-9]$")
_BQ_PROJECT_ID = re.compile(r"^[A-Za-z][A-Za-z0-9-]{4,28}[A-Za-z0-9]\Z")


def _quote_bq_project(project: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/providers/gcp/plan/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
# contain hyphens, so ``_sql_safety.validate_ident`` (which rejects ``-``)
# cannot vet them — this dedicated check guards the project component of a
# BigQuery fully-qualified name before it is interpolated into DDL/DML.
_GCP_PROJECT_RE = re.compile(r"^[a-z][a-z0-9-]{4,28}[a-z0-9]$")
_GCP_PROJECT_RE = re.compile(r"^[a-z][a-z0-9-]{4,28}[a-z0-9]\Z")


def _validate_bq_project(project: str) -> str:
Expand Down
8 changes: 4 additions & 4 deletions fluid_build/providers/gcp/util/names.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,15 +324,15 @@ def validate_name(name: str, resource_type: str) -> bool:
try:
if resource_type in ["dataset", "table"]:
# BigQuery naming rules
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*\Z", name):
return False
return len(name) <= 1024

elif resource_type == "bucket":
# Cloud Storage naming rules
if len(name) < 3 or len(name) > 63:
return False
if not re.match(r"^[a-z0-9][a-z0-9\-]*[a-z0-9]$", name):
if not re.match(r"^[a-z0-9][a-z0-9\-]*[a-z0-9]\Z", name):
return False
if re.match(r"^\d+\.\d+\.\d+\.\d+$", name):
return False
Expand All @@ -344,7 +344,7 @@ def validate_name(name: str, resource_type: str) -> bool:
return False
if not name[0].isalpha():
return False
return re.match(r"^[a-zA-Z][a-zA-Z0-9\-\._~%+]*$", name) is not None
return re.match(r"^[a-zA-Z][a-zA-Z0-9\-\._~%+]*\Z", name) is not None

elif resource_type == "job":
# Job naming rules
Expand All @@ -354,7 +354,7 @@ def validate_name(name: str, resource_type: str) -> bool:
return False
if name[-1] == "-":
return False
return re.match(r"^[a-z][a-z0-9\-]*[a-z0-9]$", name) is not None
return re.match(r"^[a-z][a-z0-9\-]*[a-z0-9]\Z", name) is not None

else:
# Unknown resource type
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/providers/odcs/mappers/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ def _root_identifier(raw: Any, version: str) -> tuple[str, Optional[str]]:
return _IMPORTED_ID_FALLBACK, None
text = str(raw)
pattern = _identifier_pattern(version)
if pattern is None or pattern.match(text):
if pattern is None or pattern.fullmatch(text):
return text, None
slug = _slugged_root_id(text)
# %r: the id is document text and may carry control characters.
Expand Down
2 changes: 1 addition & 1 deletion fluid_build/providers/odps_standard/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,7 +1141,7 @@ def _schema_defs(version: str) -> Mapping[str, Any]:

def _is_semver(value: Any, version: str) -> bool:
pattern = _semver_pattern(version)
return pattern is None or bool(pattern.match(str(value)))
return pattern is None or bool(pattern.fullmatch(str(value)))


def _rehydrate_document(fluid: Mapping[str, Any]) -> Mapping[str, Any]:
Expand Down
Loading
Loading