chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
@@ -0,0 +1,21 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def utc_date_folder(ts: float | None = None) -> str:
|
||||
t = (datetime.now(timezone.utc) if ts is None else datetime.fromtimestamp(
|
||||
ts, timezone.utc))
|
||||
return t.strftime("%Y-%m-%d")
|
||||
@@ -0,0 +1,101 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.types import PathSpec
|
||||
|
||||
_FS_STRERROR: list[tuple[type[OSError], str]] = [
|
||||
(FileNotFoundError, "No such file or directory"),
|
||||
(NotADirectoryError, "Not a directory"),
|
||||
(IsADirectoryError, "Is a directory"),
|
||||
(FileExistsError, "File exists"),
|
||||
(PermissionError, "Permission denied"),
|
||||
]
|
||||
|
||||
# The recoverable per-operand filesystem errors: every catch site that
|
||||
# formats a GNU stderr line and keeps going uses this tuple, so the catch
|
||||
# set and the strerror table can never drift apart (mirrors TS isFsError).
|
||||
FS_ERRORS: tuple[type[OSError], ...] = tuple(t for t, _ in _FS_STRERROR)
|
||||
|
||||
|
||||
def _virtual_of(path: object) -> str:
|
||||
original = getattr(path, "virtual", None)
|
||||
return original if original is not None else str(path)
|
||||
|
||||
|
||||
def enoent(path: object) -> FileNotFoundError:
|
||||
return FileNotFoundError(_virtual_of(path))
|
||||
|
||||
|
||||
def enotdir(path: object) -> NotADirectoryError:
|
||||
return NotADirectoryError(_virtual_of(path))
|
||||
|
||||
|
||||
def eisdir(path: object) -> IsADirectoryError:
|
||||
return IsADirectoryError(_virtual_of(path))
|
||||
|
||||
|
||||
def fs_strerror(exc: BaseException) -> str | None:
|
||||
for exc_type, strerror in _FS_STRERROR:
|
||||
if isinstance(exc, exc_type):
|
||||
return strerror
|
||||
return None
|
||||
|
||||
|
||||
def fs_error_line(cmd_name: str, path: object, exc: BaseException) -> str:
|
||||
"""GNU coreutils stderr line for one failed path operand.
|
||||
|
||||
Produces ``<cmd>: <path>: <strerror>``, byte-identical with the
|
||||
TypeScript formatter. ``path`` is the operand itself when the caller
|
||||
knows it (read-family commands that keep processing remaining operands
|
||||
after one fails, reported as typed via ``raw_path``), or an
|
||||
already-resolved label string.
|
||||
|
||||
Args:
|
||||
cmd_name (str): Command name for the ``<cmd>:`` prefix.
|
||||
path (object): The failed operand; ``raw_path`` (or ``virtual``) is
|
||||
the reported spelling, a plain string is used verbatim.
|
||||
exc (BaseException): The filesystem error.
|
||||
"""
|
||||
label = getattr(path, "raw_path", None) or _virtual_of(path)
|
||||
strerror = fs_strerror(exc)
|
||||
if strerror is not None:
|
||||
return f"{cmd_name}: {label}: {strerror}\n"
|
||||
return f"{cmd_name}: {label}\n"
|
||||
|
||||
|
||||
def format_fs_error(cmd_name: str,
|
||||
exc: OSError,
|
||||
paths: list[PathSpec] | None = None) -> bytes:
|
||||
"""Format a filesystem OSError as a GNU coreutils stderr line.
|
||||
|
||||
The chokepoint variant of ``fs_error_line`` for callers that only hold
|
||||
the exception: the path is recovered from it (``exc.filename`` when set,
|
||||
else ``str(exc)``); backends raise with the resolved absolute path
|
||||
(``PathSpec.virtual``). When ``paths`` is supplied, the absolute path is
|
||||
rewritten to the as-typed form (``PathSpec.raw_path``) so a relative
|
||||
argument is reported as typed, like GNU.
|
||||
|
||||
Args:
|
||||
cmd_name (str): Command name for the ``<cmd>:`` prefix.
|
||||
exc (OSError): The filesystem error.
|
||||
paths (list[PathSpec] | None): Command operands, used to map the
|
||||
resolved path back to the as-typed form.
|
||||
"""
|
||||
path = exc.filename or str(exc)
|
||||
if paths:
|
||||
for p in paths:
|
||||
if p.virtual == path:
|
||||
path = p.raw_path
|
||||
break
|
||||
return fs_error_line(cmd_name, path, exc).encode()
|
||||
@@ -0,0 +1,88 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.types import FileType
|
||||
|
||||
EXTENSION_MAP: dict[str, FileType] = {
|
||||
"json": FileType.JSON,
|
||||
"jsonl": FileType.JSON,
|
||||
"csv": FileType.CSV,
|
||||
"tsv": FileType.CSV,
|
||||
"txt": FileType.TEXT,
|
||||
"md": FileType.TEXT,
|
||||
"py": FileType.TEXT,
|
||||
"js": FileType.TEXT,
|
||||
"ts": FileType.TEXT,
|
||||
"yaml": FileType.TEXT,
|
||||
"yml": FileType.TEXT,
|
||||
"toml": FileType.TEXT,
|
||||
"png": FileType.IMAGE_PNG,
|
||||
"jpg": FileType.IMAGE_JPEG,
|
||||
"jpeg": FileType.IMAGE_JPEG,
|
||||
"gif": FileType.IMAGE_GIF,
|
||||
"zip": FileType.ZIP,
|
||||
"gz": FileType.GZIP,
|
||||
"pdf": FileType.PDF,
|
||||
"parquet": FileType.PARQUET,
|
||||
"orc": FileType.ORC,
|
||||
"feather": FileType.FEATHER,
|
||||
"arrow": FileType.FEATHER,
|
||||
"ipc": FileType.FEATHER,
|
||||
"h5": FileType.HDF5,
|
||||
"hdf5": FileType.HDF5,
|
||||
}
|
||||
|
||||
DEFAULT_TYPE = FileType.BINARY
|
||||
|
||||
_MIMETYPE_MAP: dict[str, FileType] = {
|
||||
"application/pdf": FileType.PDF,
|
||||
"application/zip": FileType.ZIP,
|
||||
"application/gzip": FileType.GZIP,
|
||||
"application/json": FileType.JSON,
|
||||
"image/png": FileType.IMAGE_PNG,
|
||||
"image/jpeg": FileType.IMAGE_JPEG,
|
||||
"image/gif": FileType.IMAGE_GIF,
|
||||
"text/csv": FileType.CSV,
|
||||
}
|
||||
|
||||
|
||||
def guess_type(path: str) -> FileType:
|
||||
"""Return the file type for *path* based on its extension.
|
||||
|
||||
Args:
|
||||
path (str): file path or name.
|
||||
|
||||
Returns:
|
||||
FileType: matched type from EXTENSION_MAP, or DEFAULT_TYPE.
|
||||
"""
|
||||
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
|
||||
return EXTENSION_MAP.get(ext, DEFAULT_TYPE)
|
||||
|
||||
|
||||
def filetype_from_mimetype(mime: str) -> FileType:
|
||||
"""Map a standard mimetype string to a FileType.
|
||||
|
||||
Args:
|
||||
mime (str): mimetype string (e.g., "image/png", "application/pdf").
|
||||
|
||||
Returns:
|
||||
FileType: matched type, TEXT for any text/*, or BINARY default.
|
||||
"""
|
||||
if not mime:
|
||||
return FileType.BINARY
|
||||
if mime in _MIMETYPE_MAP:
|
||||
return _MIMETYPE_MAP[mime]
|
||||
if mime.startswith("text/"):
|
||||
return FileType.TEXT
|
||||
return FileType.BINARY
|
||||
@@ -0,0 +1,192 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import dataclasses
|
||||
import fnmatch
|
||||
import logging
|
||||
import posixpath
|
||||
from collections.abc import Callable
|
||||
|
||||
from mirage.accessor.base import Accessor
|
||||
from mirage.cache.index import IndexCacheStore
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import rekey
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GLOB_CHARS = ("*", "?", "[")
|
||||
|
||||
|
||||
def has_glob(segment: str) -> bool:
|
||||
"""Whether a path segment contains shell glob characters.
|
||||
|
||||
Args:
|
||||
segment (str): one path component.
|
||||
"""
|
||||
return any(ch in segment for ch in GLOB_CHARS)
|
||||
|
||||
|
||||
def is_word_shaped(p: PathSpec) -> bool:
|
||||
"""Whether a pattern spec is a typed word (not a directory listing).
|
||||
|
||||
A classify-shaped word puts the pattern inside ``virtual``
|
||||
(``/data/s*/x.txt`` with directory ``/data/s*/``); a dir-shaped spec
|
||||
(``PathSpec.dir``) sets ``virtual`` to the directory itself.
|
||||
|
||||
Args:
|
||||
p (PathSpec): unresolved pattern spec.
|
||||
"""
|
||||
return p.virtual.rstrip("/") != p.directory.rstrip("/")
|
||||
|
||||
|
||||
def spell_match(raw: str, virtual: str, walked: int) -> str:
|
||||
"""Spell a match the way bash expansion would.
|
||||
|
||||
Bash rewrites only the glob segments of the typed word; everything
|
||||
before the first glob segment keeps its typed spelling, so
|
||||
``../s*/x.txt`` expands to ``../sub/x.txt``. The walked tail has the
|
||||
same segment count in the typed word and in the match's virtual
|
||||
path, so the spelling is the typed head plus the match's last
|
||||
``walked`` segments.
|
||||
|
||||
Args:
|
||||
raw (str): the pattern word as typed (``PathSpec.raw_path``).
|
||||
virtual (str): one match's absolute virtual path.
|
||||
walked (int): segment count from the first glob segment on.
|
||||
"""
|
||||
head = raw.rstrip("/").split("/")[:-walked]
|
||||
tail = virtual.rstrip("/").split("/")[-walked:]
|
||||
return "/".join([*head, *tail])
|
||||
|
||||
|
||||
async def expand_pattern(
|
||||
readdir: Callable,
|
||||
accessor: Accessor,
|
||||
path: PathSpec,
|
||||
index: IndexCacheStore | None,
|
||||
) -> list[PathSpec]:
|
||||
"""Expand a glob PathSpec segment-by-segment via readdir.
|
||||
|
||||
Mirrors bash globbing: every path component containing a glob
|
||||
character is matched against the entries of its (already expanded)
|
||||
parent directory, so a mid-path pattern (``pages/Demo_*/page.md``)
|
||||
never reaches the backend as a literal ``*`` path segment. An
|
||||
intermediate match that cannot be listed (a file, or a vanished
|
||||
entry) is skipped, matching bash's directories-only descent for
|
||||
non-final components.
|
||||
|
||||
Args:
|
||||
readdir (Callable): backend readdir ``(accessor, path, index)``
|
||||
returning absolute virtual paths.
|
||||
accessor (Accessor): backend handle passed through to readdir.
|
||||
path (PathSpec): unresolved spec whose ``resource_path`` still
|
||||
contains the pattern.
|
||||
index (IndexCacheStore | None): the per-call cache index.
|
||||
"""
|
||||
prefix = path.virtual[:len(path.virtual.rstrip("/")) -
|
||||
len(path.resource_path)]
|
||||
segments = path.resource_path.split("/") if path.resource_path else []
|
||||
# Two spec shapes reach resolvers: a full pattern path (classify), where
|
||||
# the pattern is already the last segment, and a directory-shaped spec
|
||||
# (PathSpec.dir), where the pattern applies to the directory's entries.
|
||||
if path.pattern and (not segments or segments[-1] != path.pattern):
|
||||
segments = [*segments, path.pattern]
|
||||
first = next((i for i, seg in enumerate(segments) if has_glob(seg)),
|
||||
len(segments) - 1)
|
||||
base = (prefix + "/".join(segments[:first])).rstrip("/") or "/"
|
||||
level = [base]
|
||||
for seg in segments[first:]:
|
||||
next_level: list[str] = []
|
||||
for parent in level:
|
||||
spec = PathSpec.from_str_path(
|
||||
parent, rekey(path.virtual, path.resource_path, parent))
|
||||
try:
|
||||
entries = await readdir(accessor, spec, index)
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
continue
|
||||
next_level.extend(
|
||||
e for e in entries
|
||||
if fnmatch.fnmatch(e.rstrip("/").rsplit("/", 1)[-1], seg))
|
||||
level = next_level
|
||||
if not level:
|
||||
return []
|
||||
matches = [
|
||||
PathSpec.from_str_path(e, rekey(path.virtual, path.resource_path, e))
|
||||
for e in level
|
||||
]
|
||||
# A typed word (raw differs from virtual) spells its matches; the
|
||||
# dir-shaped specs internal expansions build (PathSpec.dir) have no
|
||||
# typed form and keep the resolved virtual.
|
||||
if path.raw_path == path.virtual:
|
||||
return matches
|
||||
walked = len(segments) - first
|
||||
return [
|
||||
dataclasses.replace(m,
|
||||
raw_path=spell_match(path.raw_path, m.virtual,
|
||||
walked)) for m in matches
|
||||
]
|
||||
|
||||
|
||||
async def resolve_glob_with(
|
||||
readdir: Callable,
|
||||
accessor: Accessor,
|
||||
paths: list[PathSpec],
|
||||
index: IndexCacheStore | None,
|
||||
cap: int | None = None,
|
||||
) -> list[PathSpec]:
|
||||
"""Shared resolve_glob loop over a backend's readdir.
|
||||
|
||||
Resolved specs pass through, pattern specs expand segment-by-segment
|
||||
via :func:`expand_pattern` (mid-path aware, spelled as typed), an
|
||||
unmatched glob word stays the literal (bash with nullglob off: the
|
||||
command then errors on it like GNU), and matches cap at ``cap`` when
|
||||
given. Per-backend glob modules bind their own readdir.
|
||||
|
||||
Args:
|
||||
readdir (Callable): backend readdir ``(accessor, path, index)``
|
||||
returning absolute virtual paths.
|
||||
accessor (Accessor): backend handle passed through to readdir.
|
||||
paths (list[PathSpec]): specs to resolve.
|
||||
index (IndexCacheStore | None): the per-call cache index.
|
||||
cap (int | None): cap on matches per pattern before truncation.
|
||||
"""
|
||||
result: list[PathSpec] = []
|
||||
for p in paths:
|
||||
if isinstance(p, str):
|
||||
result.append(
|
||||
PathSpec(virtual=p,
|
||||
directory=posixpath.dirname(p),
|
||||
resource_path=p.strip("/")))
|
||||
continue
|
||||
if p.resolved:
|
||||
result.append(p)
|
||||
elif p.pattern:
|
||||
matched = await expand_pattern(readdir, accessor, p, index)
|
||||
if not matched and is_word_shaped(p):
|
||||
# bash with nullglob off: an unmatched glob word stays
|
||||
# the literal; the command then errors on it like GNU
|
||||
# (cat '*.nope' -> No such file or directory, exit 1).
|
||||
# Dir-shaped specs (PathSpec.dir) are internal
|
||||
# expansions and keep the empty result.
|
||||
result.append(
|
||||
dataclasses.replace(p, pattern=None, resolved=True))
|
||||
continue
|
||||
if cap is not None and len(matched) > cap:
|
||||
logger.warning("%s: %d matches exceeds limit (%d), truncating",
|
||||
p.directory, len(matched), cap)
|
||||
matched = matched[:cap]
|
||||
result.extend(matched)
|
||||
else:
|
||||
result.append(p)
|
||||
return result
|
||||
@@ -0,0 +1,168 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
|
||||
def normalize(raw: str | None) -> str:
|
||||
"""Normalize a key prefix.
|
||||
|
||||
Args:
|
||||
raw: The raw prefix string, or None.
|
||||
|
||||
Returns:
|
||||
Empty string if input was None/empty; otherwise the prefix with
|
||||
leading slashes stripped and a trailing slash ensured.
|
||||
"""
|
||||
if not raw:
|
||||
return ""
|
||||
v = raw.lstrip("/")
|
||||
return v if v.endswith("/") else v + "/"
|
||||
|
||||
|
||||
def apply(prefix: str, path: str) -> str:
|
||||
"""Prepend a normalized prefix to a virtual path.
|
||||
|
||||
Args:
|
||||
prefix: A normalized prefix (use ``normalize()`` first if unsure).
|
||||
path: The virtual path to scope.
|
||||
|
||||
Returns:
|
||||
The backend key: ``prefix + path`` with the leading slash of
|
||||
``path`` stripped.
|
||||
"""
|
||||
return prefix + path.lstrip("/")
|
||||
|
||||
|
||||
def apply_dir(prefix: str, path: str) -> str:
|
||||
"""Same as ``apply()`` but guarantees a trailing slash for LIST-style ops.
|
||||
|
||||
Args:
|
||||
prefix: A normalized prefix.
|
||||
path: The virtual path to scope.
|
||||
|
||||
Returns:
|
||||
The backend key with a trailing slash, suitable for use as a LIST
|
||||
``Prefix`` argument.
|
||||
"""
|
||||
key = apply(prefix, path)
|
||||
return key if not key or key.endswith("/") else key + "/"
|
||||
|
||||
|
||||
def strip(prefix: str, key: str) -> str:
|
||||
"""Strip a normalized prefix from a backend-returned key.
|
||||
|
||||
Args:
|
||||
prefix: A normalized prefix.
|
||||
key: The backend-returned key.
|
||||
|
||||
Returns:
|
||||
The key with the prefix removed if present; otherwise unchanged.
|
||||
"""
|
||||
return key[len(prefix):] if prefix and key.startswith(prefix) else key
|
||||
|
||||
|
||||
def strip_mount(virtual: str, prefix: str) -> str:
|
||||
"""Remove a mount prefix from a virtual path at a path boundary.
|
||||
|
||||
A sibling that only shares the prefix as a string (``/database`` vs a
|
||||
``/data`` prefix) is left untouched.
|
||||
|
||||
Args:
|
||||
virtual (str): An absolute virtual path.
|
||||
prefix (str): The mount prefix (e.g. ``/data``), without a trailing
|
||||
slash.
|
||||
|
||||
Returns:
|
||||
The mount-relative path with its leading slash kept.
|
||||
|
||||
Example::
|
||||
|
||||
strip_mount("/data/sub/x.txt", "/data") -> "/sub/x.txt"
|
||||
strip_mount("/database/x.txt", "/data") -> "/database/x.txt"
|
||||
strip_mount("/data", "/data") -> "/"
|
||||
strip_mount("/x.txt", "") -> "/x.txt"
|
||||
"""
|
||||
if prefix and virtual.startswith(prefix):
|
||||
rest = virtual[len(prefix):]
|
||||
if prefix.endswith("/") or rest == "" or rest.startswith("/"):
|
||||
return rest or "/"
|
||||
return virtual
|
||||
|
||||
|
||||
def mount_key(virtual: str, prefix: str) -> str:
|
||||
"""Backend key for a virtual path under a mount prefix.
|
||||
|
||||
Args:
|
||||
virtual (str): An absolute virtual path.
|
||||
prefix (str): The mount prefix.
|
||||
|
||||
Returns:
|
||||
The mount-relative path with surrounding slashes stripped.
|
||||
|
||||
Example::
|
||||
|
||||
mount_key("/data/sub/x.txt", "/data") -> "sub/x.txt"
|
||||
mount_key("/data", "/data") -> ""
|
||||
mount_key("/x.txt", "") -> "x.txt"
|
||||
"""
|
||||
return strip_mount(virtual, prefix).strip("/")
|
||||
|
||||
|
||||
def rekey(parent_original: str, parent_key: str, child: str) -> str:
|
||||
"""Backend key for a child virtual path, derived from its parent.
|
||||
|
||||
A child shares the parent's mount prefix, so its key is the child
|
||||
virtual path with the same prefix removed. The prefix length is
|
||||
recovered from the parent's ``original``/``key`` pair, so no mount
|
||||
context is needed.
|
||||
|
||||
Args:
|
||||
parent_original (str): The parent's absolute virtual path.
|
||||
parent_key (str): The parent's backend key.
|
||||
child (str): The child's absolute virtual path.
|
||||
|
||||
Returns:
|
||||
The child's backend key (surrounding slashes stripped).
|
||||
|
||||
Example::
|
||||
|
||||
rekey("/data/sub", "sub", "/data/sub/x.txt") -> "sub/x.txt"
|
||||
rekey("/data", "", "/data/x.txt") -> "x.txt"
|
||||
"""
|
||||
prefix_len = len(parent_original.rstrip("/")) - len(parent_key)
|
||||
return child[prefix_len:].strip("/")
|
||||
|
||||
|
||||
def mount_prefix_of(virtual: str, resource_path: str) -> str:
|
||||
"""Recover a mount prefix from a virtual path and its backend key.
|
||||
|
||||
The inverse of stamping: given a path's virtual form and the key the
|
||||
mount stamped, return the mount prefix that was stripped off. Used by
|
||||
commands (e.g. ``find``) that must map backend keys back to virtual
|
||||
paths for display.
|
||||
|
||||
Args:
|
||||
virtual (str): An absolute virtual path.
|
||||
resource_path (str): Its backend key (mount-relative, slashless).
|
||||
|
||||
Returns:
|
||||
The mount prefix without a trailing slash.
|
||||
|
||||
Example::
|
||||
|
||||
mount_prefix_of("/data/sub", "sub") -> "/data"
|
||||
mount_prefix_of("/data", "") -> "/data"
|
||||
mount_prefix_of("/x.txt", "x.txt") -> ""
|
||||
"""
|
||||
prefix_len = len(virtual.rstrip("/")) - len(resource_path)
|
||||
return virtual[:prefix_len].rstrip("/")
|
||||
@@ -0,0 +1,82 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.utils.sanitize import path_safe_name, sanitize_name
|
||||
|
||||
|
||||
def make_id_name(
|
||||
display_name: str,
|
||||
resource_id: str,
|
||||
*,
|
||||
path_safe: bool = False,
|
||||
) -> str:
|
||||
"""Build a name with embedded ID for VFS paths.
|
||||
|
||||
Used by resources that encode resource IDs in filenames
|
||||
for reverse lookups (Discord, Slack, Linear, Trello).
|
||||
|
||||
By default applies the full ``sanitize_name`` transform: replaces
|
||||
unsafe shell chars and spaces with underscores. Set
|
||||
``path_safe=True`` to preserve the original spelling (apostrophes,
|
||||
spaces, emoji) and only replace ``/`` with ``∕``. Discord and
|
||||
Slack use ``path_safe=True`` so display names stay readable.
|
||||
|
||||
Example::
|
||||
|
||||
make_id_name("general", "C123456")
|
||||
→ "general__C123456"
|
||||
make_id_name("My Project!", "uuid-abc")
|
||||
→ "My_Project__uuid-abc"
|
||||
make_id_name("Zecheng's Server", "G1", path_safe=True)
|
||||
→ "Zecheng's Server__G1"
|
||||
|
||||
Args:
|
||||
display_name (str): human-readable name from the API.
|
||||
resource_id (str): resource-specific unique ID.
|
||||
path_safe (bool): if True, preserve spelling and only escape
|
||||
the path separator. Otherwise apply full sanitization.
|
||||
"""
|
||||
transform = path_safe_name if path_safe else sanitize_name
|
||||
return f"{transform(display_name)}__{resource_id}"
|
||||
|
||||
|
||||
def parse_id_name(
|
||||
name: str,
|
||||
*,
|
||||
suffix: str = "",
|
||||
) -> tuple[str, str]:
|
||||
"""Extract (display_name, resource_id) from make_id_name output.
|
||||
|
||||
Example::
|
||||
|
||||
parse_id_name("general__C123456")
|
||||
→ ("general", "C123456")
|
||||
parse_id_name("team__uuid.json", suffix=".json")
|
||||
→ ("team", "uuid")
|
||||
|
||||
Args:
|
||||
name (str): filename with embedded ID.
|
||||
suffix (str): file extension to strip before parsing.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: if name doesn't contain "__" or doesn't end
|
||||
with ``suffix``.
|
||||
"""
|
||||
if suffix and not name.endswith(suffix):
|
||||
raise FileNotFoundError(name)
|
||||
raw = name[:-len(suffix)] if suffix else name
|
||||
label, sep, resource_id = raw.rpartition("__")
|
||||
if not sep or not resource_id:
|
||||
raise FileNotFoundError(name)
|
||||
return label, resource_id
|
||||
@@ -0,0 +1,239 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import posixpath
|
||||
|
||||
|
||||
def norm(path: str) -> str:
|
||||
"""Normalize a virtual path to a leading-slash, no-trailing-slash key.
|
||||
|
||||
Args:
|
||||
path: A virtual path string.
|
||||
|
||||
Returns:
|
||||
The path with surrounding slashes collapsed to a single leading
|
||||
slash (``"foo/bar/"`` -> ``"/foo/bar"``, ``""`` -> ``"/"``).
|
||||
"""
|
||||
return "/" + path.strip("/")
|
||||
|
||||
|
||||
def parent(path: str) -> str:
|
||||
"""Return the parent directory of a normalized virtual key.
|
||||
|
||||
Args:
|
||||
path: A normalized virtual path (leading slash, no trailing slash).
|
||||
|
||||
Returns:
|
||||
The path with its last segment removed (``"/a/b"`` -> ``"/a"``),
|
||||
or ``"/"`` when there is no parent segment.
|
||||
"""
|
||||
i = path.rfind("/")
|
||||
return path[:i] if i > 0 else "/"
|
||||
|
||||
|
||||
def resolve_path(path: str, cwd: str) -> str:
|
||||
"""Resolve a relative path against cwd.
|
||||
|
||||
Example::
|
||||
|
||||
resolve_path("../file.txt", "/data/sub/")
|
||||
→ "/data/file.txt"
|
||||
resolve_path("/abs/path", "/ignored")
|
||||
→ "/abs/path"
|
||||
"""
|
||||
if not path.startswith("/"):
|
||||
path = cwd.rstrip("/") + "/" + path
|
||||
resolved = posixpath.normpath(path)
|
||||
if resolved.startswith("//"):
|
||||
resolved = "/" + resolved.lstrip("/")
|
||||
return resolved
|
||||
|
||||
|
||||
MAX_SYMLINK_HOPS = 40
|
||||
|
||||
|
||||
class CycleError(Exception):
|
||||
"""Raised when symlink resolution exceeds the maximum hop count.
|
||||
|
||||
Mirrors POSIX ELOOP (a loop such as ``a -> b -> a`` or an unbounded
|
||||
expansion such as ``a -> a/x``). Command boundaries render this as the
|
||||
GNU ``strerror`` text "Too many levels of symbolic links".
|
||||
"""
|
||||
|
||||
|
||||
def _is_link_prefix(key: str, path: str) -> bool:
|
||||
return path == key or path.startswith(key + "/")
|
||||
|
||||
|
||||
def resolve_symlinks(path: str, links: dict[str, str]) -> str:
|
||||
"""Resolve symlink prefixes in ``path`` until stable.
|
||||
|
||||
Repeatedly replaces the longest dict key that is a path-boundary prefix
|
||||
of ``path`` with its target, mirroring filesystem symlink following
|
||||
(``/a/b`` is a prefix of ``/a/b/c`` but not ``/a/bc``). Relative targets
|
||||
are resolved against the link's own parent directory.
|
||||
|
||||
Args:
|
||||
path (str): An absolute virtual path.
|
||||
links (dict[str, str]): Map of link virtual-path to target.
|
||||
|
||||
Returns:
|
||||
str: The path with all symlink prefixes resolved.
|
||||
|
||||
Raises:
|
||||
CycleError: If resolution exceeds ``MAX_SYMLINK_HOPS`` (a loop or
|
||||
unbounded expansion), matching POSIX ELOOP.
|
||||
"""
|
||||
if not links:
|
||||
return path
|
||||
for _ in range(MAX_SYMLINK_HOPS):
|
||||
best: str | None = None
|
||||
for key in links:
|
||||
if _is_link_prefix(key, path) and (best is None
|
||||
or len(key) > len(best)):
|
||||
best = key
|
||||
if best is None:
|
||||
return path
|
||||
target = links[best]
|
||||
if not target.startswith("/"):
|
||||
target = norm(parent(best) + "/" + target)
|
||||
path = target + path[len(best):]
|
||||
raise CycleError(path)
|
||||
|
||||
|
||||
def expand_tilde(word: str, home: str | None) -> str:
|
||||
"""Expand a leading ``~`` against the home directory.
|
||||
|
||||
``~`` alone or ``~/rest`` expands to ``home`` (or ``home/rest``).
|
||||
``~user`` and any non-leading ``~`` are left unchanged, matching
|
||||
bash behavior when no matching user exists. When ``home`` is ``None``
|
||||
(``$HOME`` unset/empty), a leading ``~`` is left literal, mirroring
|
||||
GNU bash with no home directory.
|
||||
|
||||
Args:
|
||||
word: The unexpanded word.
|
||||
home: The home directory to substitute for ``~``, or ``None``.
|
||||
|
||||
Returns:
|
||||
The word with a leading ``~`` resolved, or the word unchanged.
|
||||
"""
|
||||
if home is None:
|
||||
return word
|
||||
if word == "~":
|
||||
return home
|
||||
if word.startswith("~/"):
|
||||
return home.rstrip("/") + word[1:]
|
||||
return word
|
||||
|
||||
|
||||
def rebase_raw(paths: list[str], original: str, raw: str) -> list[str]:
|
||||
"""Rewrite the base of walked output paths to the as-typed form.
|
||||
|
||||
Used by walkers like ``find``/``grep -r``: results are absolute (start
|
||||
path plus subpath), but when the start path was typed relatively the
|
||||
output should show it that way. Maps :func:`rebase_one` over ``paths``.
|
||||
|
||||
Because :func:`rebase_one` only rewrites the leading base prefix, this
|
||||
also works on formatted lines whose path is the prefix, e.g. grep's
|
||||
``path:line``.
|
||||
|
||||
Example::
|
||||
|
||||
rebase_raw(["/data/sub/x", "/data/y"], "/data", ".")
|
||||
-> ["./sub/x", "./y"]
|
||||
rebase_raw(["/data/sub/x:hit"], "/data/sub", "sub")
|
||||
-> ["sub/x:hit"]
|
||||
rebase_raw(["/data/x"], "/data", "/data") # absolute arg
|
||||
-> ["/data/x"] # unchanged
|
||||
|
||||
Args:
|
||||
paths (list[str]): Absolute result paths (or ``path:...`` lines)
|
||||
produced by walking ``original``.
|
||||
original (str): The resolved absolute start path.
|
||||
raw (str): The as-typed start path (``PathSpec.raw_path``); equal
|
||||
to ``original`` leaves ``paths`` unchanged (the
|
||||
absolute-argument case).
|
||||
|
||||
Returns:
|
||||
list[str]: ``paths`` with each ``original`` base replaced by
|
||||
``raw``.
|
||||
"""
|
||||
if raw == original:
|
||||
return paths
|
||||
return [rebase_one(p, original, raw) for p in paths]
|
||||
|
||||
|
||||
def rebase_one(path: str, original: str, raw: str) -> str:
|
||||
"""Rewrite a single path's ``original`` base to the as-typed ``raw``.
|
||||
|
||||
Only the leading ``original`` prefix is rewritten, so any suffix after
|
||||
the path (e.g. grep's ``:line``) is preserved untouched.
|
||||
|
||||
Example::
|
||||
|
||||
rebase_one("/data/sub/x", "/data", ".") -> "./sub/x"
|
||||
rebase_one("/data/sub", "/data/sub", "sub") -> "sub"
|
||||
rebase_one("/data/x:hit", "/data", ".") -> "./x:hit"
|
||||
rebase_one("/other/x", "/data", ".") -> "/other/x" # no match
|
||||
rebase_one("/data/x", "/data", "/data") -> "/data/x" # absolute
|
||||
|
||||
Args:
|
||||
path (str): An absolute path at or under ``original`` (optionally with
|
||||
a trailing ``:...`` suffix).
|
||||
original (str): The resolved absolute base (traversal root).
|
||||
raw (str): The as-typed base (``PathSpec.raw_path``); equal to
|
||||
``original`` leaves ``path`` unchanged.
|
||||
|
||||
Returns:
|
||||
str: ``path`` with its ``original`` base replaced by ``raw``.
|
||||
"""
|
||||
if raw == original:
|
||||
return path
|
||||
base = original.rstrip("/")
|
||||
if path == base:
|
||||
return raw
|
||||
if path.startswith(base + "/"):
|
||||
return raw.rstrip("/") + path[len(base):]
|
||||
return path
|
||||
|
||||
|
||||
def gnu_basename(path: str, suffix: str | None = None) -> str:
|
||||
i = len(path)
|
||||
while i > 0 and path[i - 1] == "/":
|
||||
i -= 1
|
||||
if i == 0:
|
||||
return "/" if path else ""
|
||||
j = path.rfind("/", 0, i)
|
||||
base = path[j + 1:i]
|
||||
if suffix and base != suffix and base.endswith(suffix):
|
||||
base = base[:len(base) - len(suffix)]
|
||||
return base
|
||||
|
||||
|
||||
def gnu_dirname(path: str) -> str:
|
||||
if path == "":
|
||||
return "."
|
||||
i = len(path)
|
||||
while i > 0 and path[i - 1] == "/":
|
||||
i -= 1
|
||||
if i == 0:
|
||||
return "/"
|
||||
j = path.rfind("/", 0, i)
|
||||
if j == -1:
|
||||
return "."
|
||||
while j > 0 and path[j - 1] == "/":
|
||||
j -= 1
|
||||
if j == 0:
|
||||
return "/"
|
||||
return path[:j]
|
||||
@@ -0,0 +1,63 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import re
|
||||
|
||||
UNSAFE_CHARS = re.compile(r"[^\w\s\-.]")
|
||||
MULTI_UNDERSCORE = re.compile(r"_+")
|
||||
MAX_LEN = 100
|
||||
|
||||
|
||||
def sanitize_name(name: str) -> str:
|
||||
"""Sanitize a name for use in virtual paths.
|
||||
|
||||
Replaces shell-unsafe characters (apostrophes, quotes, etc.)
|
||||
and spaces with underscores. Safe for use in shell commands
|
||||
without quoting.
|
||||
|
||||
Args:
|
||||
name (str): raw name from API.
|
||||
|
||||
Returns:
|
||||
str: sanitized name.
|
||||
"""
|
||||
if not name.strip():
|
||||
return "unknown"
|
||||
cleaned = UNSAFE_CHARS.sub("_", name)
|
||||
cleaned = cleaned.replace(" ", "_")
|
||||
cleaned = MULTI_UNDERSCORE.sub("_", cleaned)
|
||||
cleaned = cleaned.strip("_")
|
||||
if len(cleaned) > MAX_LEN:
|
||||
cleaned = cleaned[:MAX_LEN]
|
||||
return cleaned
|
||||
|
||||
|
||||
def path_safe_name(name: str) -> str:
|
||||
"""Make a name safe to embed in a VFS path segment.
|
||||
|
||||
Preserves the original spelling (spaces, apostrophes, emoji, etc.)
|
||||
and only replaces the path separator ``/`` with ``∕`` (U+2215)
|
||||
so the value cannot collide with a directory boundary. Use this
|
||||
for resource directory and file names where keeping the original
|
||||
display name matters more than shell ergonomics.
|
||||
|
||||
Args:
|
||||
name (str): raw name from API.
|
||||
|
||||
Returns:
|
||||
str: path-safe name, or "unknown" if empty.
|
||||
"""
|
||||
if not name.strip():
|
||||
return "unknown"
|
||||
return name.replace("/", "∕")
|
||||
@@ -0,0 +1,37 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
|
||||
def score_from_distance(value: object) -> str:
|
||||
"""Convert a vector distance into a clamped 0..1 similarity string.
|
||||
|
||||
Args:
|
||||
value (object): Distance reported by the vector store; anything
|
||||
non-numeric (including bool) collapses to "0.00".
|
||||
"""
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
return "0.00"
|
||||
return f"{max(0.0, 1.0 - float(value)):.2f}"
|
||||
|
||||
|
||||
def format_score(value: object) -> str | None:
|
||||
"""Format a provider-supplied similarity score to two decimals.
|
||||
|
||||
Args:
|
||||
value (object): Score reported by the provider; anything
|
||||
non-numeric (including bool) yields None.
|
||||
"""
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
return None
|
||||
return f"{value:.2f}"
|
||||
@@ -0,0 +1,19 @@
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
|
||||
async def ensure_stream(
|
||||
src: bytes | AsyncIterator[bytes]) -> AsyncIterator[bytes]:
|
||||
if isinstance(src, bytes):
|
||||
yield src
|
||||
return
|
||||
async for chunk in src:
|
||||
yield chunk
|
||||
|
||||
|
||||
async def collect_bytes(src: bytes | AsyncIterator[bytes]) -> bytes:
|
||||
if isinstance(src, bytes):
|
||||
return src
|
||||
chunks: list[bytes] = []
|
||||
async for chunk in src:
|
||||
chunks.append(chunk)
|
||||
return b"".join(chunks)
|
||||
Reference in New Issue
Block a user