Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66f649ea3f | |||
| 0727bd75a5 |
@@ -19,7 +19,7 @@ from mirage import MountMode, Workspace
|
||||
from mirage.sdk import (NULL_INDEX, Accessor, CommandIO, CommandSpec, FileStat,
|
||||
GenericResource, IndexCacheStore, IOResult, PathSpec,
|
||||
command, register_resource, stream_from_bytes)
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType, FileType
|
||||
|
||||
# A whole custom backend in one script: three async core functions over
|
||||
# your data source, one CommandIO table, one GenericResource. Every
|
||||
@@ -84,7 +84,10 @@ async def stat(
|
||||
name = path.virtual.rstrip("/").rsplit("/", 1)[-1] or "/"
|
||||
if isinstance(node, dict):
|
||||
return FileStat(name=name, size=None, type=FileType.DIRECTORY)
|
||||
return FileStat(name=name, size=len(node.encode()), type=FileType.TEXT)
|
||||
return FileStat(name=name,
|
||||
size=len(node.encode()),
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
|
||||
# Optional: a bespoke domain verb, registered alongside the generics.
|
||||
|
||||
@@ -18,20 +18,20 @@ from typing import Any, cast
|
||||
from agents import Runner, TResponseInputItem
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
from mirage.workspace.workspace import Workspace
|
||||
|
||||
_VISION_TYPES = {
|
||||
FileType.IMAGE_PNG,
|
||||
FileType.IMAGE_JPEG,
|
||||
FileType.IMAGE_GIF,
|
||||
ContentType.IMAGE_PNG,
|
||||
ContentType.IMAGE_JPEG,
|
||||
ContentType.IMAGE_GIF,
|
||||
}
|
||||
|
||||
_MIMETYPE_FOR = {
|
||||
FileType.IMAGE_PNG: "image/png",
|
||||
FileType.IMAGE_JPEG: "image/jpeg",
|
||||
FileType.IMAGE_GIF: "image/gif",
|
||||
FileType.PDF: "application/pdf",
|
||||
ContentType.IMAGE_PNG: "image/png",
|
||||
ContentType.IMAGE_JPEG: "image/jpeg",
|
||||
ContentType.IMAGE_GIF: "image/gif",
|
||||
ContentType.PDF: "application/pdf",
|
||||
}
|
||||
|
||||
|
||||
@@ -56,14 +56,14 @@ class MirageRunner:
|
||||
async def _block_for_path(self, path: str) -> dict[str, Any]:
|
||||
st = await self._ws.ops.stat(path)
|
||||
data = await self._ws.ops.read(path)
|
||||
if st.type in _VISION_TYPES:
|
||||
mime = _MIMETYPE_FOR[st.type]
|
||||
if st.content in _VISION_TYPES:
|
||||
mime = _MIMETYPE_FOR[st.content]
|
||||
b64 = base64.b64encode(data).decode("ascii")
|
||||
return {
|
||||
"type": "input_image",
|
||||
"image_url": f"data:{mime};base64,{b64}",
|
||||
}
|
||||
if st.type == FileType.PDF:
|
||||
if st.content == ContentType.PDF:
|
||||
if self._client is None:
|
||||
self._client = AsyncOpenAI()
|
||||
filename = path.rsplit("/", 1)[-1]
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.commands.builtin.constants import FILE_MIME_MAP
|
||||
from mirage.types import FileStat, FileType
|
||||
from mirage.types import ContentType, FileStat, FileType
|
||||
|
||||
|
||||
def format_file_result(
|
||||
path: str,
|
||||
result: FileType | str,
|
||||
result: ContentType | FileType | str,
|
||||
brief: bool,
|
||||
mime: bool,
|
||||
) -> str:
|
||||
@@ -26,33 +26,35 @@ def format_file_result(
|
||||
|
||||
Args:
|
||||
path (str): operand as typed, omitted under -b.
|
||||
result (FileType | str): detected type, or a ready description.
|
||||
result (ContentType | FileType | str): detected type, or a ready
|
||||
description.
|
||||
brief (bool): -b, drop the filename column.
|
||||
mime (bool): -i, map the type to its MIME spelling.
|
||||
"""
|
||||
key = result.value if isinstance(result, FileType) else str(result)
|
||||
key = (result.value if isinstance(result, (ContentType,
|
||||
FileType)) else str(result))
|
||||
desc = FILE_MIME_MAP.get(key, key) if mime else key
|
||||
if brief:
|
||||
return desc
|
||||
return f"{path}: {desc}"
|
||||
|
||||
|
||||
def _detect(path: str, header: bytes, s: FileStat) -> FileType | str:
|
||||
if s.type and s.type != FileType.BINARY:
|
||||
return s.type
|
||||
magic: list[tuple[bytes, FileType]] = [
|
||||
(b"\x89PNG", FileType.IMAGE_PNG),
|
||||
(b"\xff\xd8\xff", FileType.IMAGE_JPEG),
|
||||
(b"GIF8", FileType.IMAGE_GIF),
|
||||
(b"PK\x03\x04", FileType.ZIP),
|
||||
(b"\x1f\x8b", FileType.GZIP),
|
||||
(b"%PDF", FileType.PDF),
|
||||
(b"{\n", FileType.JSON),
|
||||
(b"[{", FileType.JSON),
|
||||
def _detect(path: str, header: bytes, s: FileStat) -> ContentType | str:
|
||||
if s.content is not None and s.content != ContentType.BINARY:
|
||||
return s.content
|
||||
magic: list[tuple[bytes, ContentType]] = [
|
||||
(b"\x89PNG", ContentType.IMAGE_PNG),
|
||||
(b"\xff\xd8\xff", ContentType.IMAGE_JPEG),
|
||||
(b"GIF8", ContentType.IMAGE_GIF),
|
||||
(b"PK\x03\x04", ContentType.ZIP),
|
||||
(b"\x1f\x8b", ContentType.GZIP),
|
||||
(b"%PDF", ContentType.PDF),
|
||||
(b"{\n", ContentType.JSON),
|
||||
(b"[{", ContentType.JSON),
|
||||
]
|
||||
for sig, ftype in magic:
|
||||
if header.startswith(sig):
|
||||
return ftype
|
||||
if all(b < 128 for b in header[:256] if b != 0):
|
||||
return FileType.TEXT
|
||||
return FileType.BINARY
|
||||
return ContentType.TEXT
|
||||
return ContentType.BINARY
|
||||
|
||||
@@ -22,10 +22,7 @@ _ASCII_DIGITS = frozenset("0123456789")
|
||||
_TYPE_LABELS = {
|
||||
FileType.DIRECTORY: "directory",
|
||||
FileType.SYMLINK: "symbolic link",
|
||||
FileType.TEXT: "regular file",
|
||||
FileType.BINARY: "regular file",
|
||||
FileType.JSON: "regular file",
|
||||
FileType.CSV: "regular file",
|
||||
FileType.FILE: "regular file",
|
||||
}
|
||||
|
||||
_DEFAULT_OWNER = "user"
|
||||
@@ -352,8 +349,12 @@ def _render_stat(s: FileStat) -> str:
|
||||
Args:
|
||||
s (FileStat): the stat to render.
|
||||
"""
|
||||
# The record's type= shows a regular file's content shape and a
|
||||
# non-regular node's kind, so one field reads the way it always has.
|
||||
shown = (s.content.value if s.type is FileType.FILE
|
||||
and s.content is not None else s.type.value)
|
||||
return (f"name={s.name} size={s.size} modified={s.modified}"
|
||||
f" type={s.type.value if s.type else None}")
|
||||
f" type={shown}")
|
||||
|
||||
|
||||
async def stat(
|
||||
|
||||
@@ -49,7 +49,8 @@ def _stat_from_item(item: dict[str, Any]) -> FileStat:
|
||||
return FileStat(
|
||||
name=vfs_name,
|
||||
size=item.get("size"),
|
||||
type=guess_type(vfs_name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(vfs_name),
|
||||
modified=remote_time,
|
||||
fingerprint=sha1 or remote_time or None,
|
||||
extra={
|
||||
@@ -118,7 +119,8 @@ async def stat(
|
||||
return FileStat(
|
||||
name=result.entry.vfs_name or result.entry.name,
|
||||
size=result.entry.size,
|
||||
type=guess_type(result.entry.vfs_name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(result.entry.vfs_name),
|
||||
modified=result.entry.remote_time,
|
||||
fingerprint=sha1 or result.entry.remote_time or None,
|
||||
extra={
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.chroma.path import resolve_path
|
||||
from mirage.core.chroma.sizes import ensure_dir_sizes
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.path import parent
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ async def stat(accessor,
|
||||
entry = refreshed.entry
|
||||
return FileStat(
|
||||
name=entry.name,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=entry.size,
|
||||
modified=entry.extra.get("updated_at"),
|
||||
fingerprint=None,
|
||||
|
||||
@@ -85,7 +85,8 @@ async def stat(
|
||||
return FileStat(name=entry.name,
|
||||
size=entry.size,
|
||||
modified=entry.remote_time or None,
|
||||
type=guess_type(entry.name))
|
||||
type=FileType.FILE,
|
||||
content=guess_type(entry.name))
|
||||
parent = virtual_key.rsplit("/", 1)[0] or "/"
|
||||
parent_listing = await index.list_dir(parent)
|
||||
if parent_listing.entries is not None:
|
||||
@@ -106,4 +107,5 @@ async def stat(
|
||||
return FileStat(name=name,
|
||||
size=size,
|
||||
modified=modified,
|
||||
type=guess_type(name))
|
||||
type=FileType.FILE,
|
||||
content=guess_type(name))
|
||||
|
||||
@@ -5,7 +5,7 @@ from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.dify.client import get_document_detail
|
||||
from mirage.core.dify.path import resolve_path
|
||||
from mirage.core.dify.tree import extract_document_size
|
||||
from mirage.types import FileStat, FileType, JsonValue, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, JsonValue, PathSpec
|
||||
|
||||
|
||||
async def stat_light(accessor: DifyAccessor,
|
||||
@@ -27,7 +27,8 @@ async def stat_light(accessor: DifyAccessor,
|
||||
extra["source_size"] = resolved.entry.size
|
||||
return FileStat(
|
||||
name=resolved.entry.name,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=None,
|
||||
modified=timestamp_to_zulu(resolved.entry.remote_time),
|
||||
fingerprint=None,
|
||||
@@ -64,7 +65,8 @@ async def stat(accessor: DifyAccessor,
|
||||
extra["indexing_status"] = detail.get("indexing_status")
|
||||
return FileStat(
|
||||
name=resolved.entry.name,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=None,
|
||||
modified=timestamp_to_zulu(detail.get("updated_at")),
|
||||
fingerprint=None,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from mirage.core.hierarchy.codec import DATE, JSON_NAME
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
_GUILD = (Slot("guild", id_key="guild_id"), )
|
||||
_CHANNEL = _GUILD + ("channels", Slot("channel", id_key="channel_id"))
|
||||
@@ -34,12 +34,12 @@ SCOPES = (
|
||||
segments=_GUILD +
|
||||
("members", Slot("member", JSON_NAME, id_key="user_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="day", segments=_DAY),
|
||||
Scope(kind="messages",
|
||||
segments=_DAY + ("chat.jsonl", ),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
Scope(kind="files", segments=_DAY + ("files", )),
|
||||
Scope(kind="file_blob", segments=_DAY + ("files", Slot("blob")),
|
||||
leaf=True),
|
||||
|
||||
@@ -20,7 +20,7 @@ from mirage.core.discord.scope import detect_scope
|
||||
from mirage.core.hierarchy.probe import resolve_entry
|
||||
from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.stat import entry_stat, make_stat
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
from mirage.utils.filetype import filetype_from_mimetype
|
||||
from mirage.utils.key_prefix import mount_key, mount_prefix_of
|
||||
@@ -56,7 +56,8 @@ def _file_blob_stat(match: ScopeMatch, path: PathSpec,
|
||||
return FileStat(
|
||||
name=entry.vfs_name or entry.name,
|
||||
size=entry.size,
|
||||
type=filetype_from_mimetype(mimetype),
|
||||
type=FileType.FILE,
|
||||
content=filetype_from_mimetype(mimetype),
|
||||
extra={
|
||||
"content_type": mimetype,
|
||||
"attachment_id": entry.id,
|
||||
@@ -123,9 +124,15 @@ async def _stat_chat(accessor: DiscordAccessor, match: ScopeMatch,
|
||||
"""
|
||||
entry = await resolve_entry(readdir, accessor, path, index)
|
||||
if entry is not None:
|
||||
return FileStat(name="chat.jsonl", type=FileType.TEXT, size=entry.size)
|
||||
return FileStat(name="chat.jsonl",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=entry.size)
|
||||
await _channel_proven(accessor, path, index, up=2)
|
||||
return FileStat(name="chat.jsonl", type=FileType.TEXT, size=None)
|
||||
return FileStat(name="chat.jsonl",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=None)
|
||||
|
||||
|
||||
stat = make_stat(
|
||||
@@ -136,7 +143,7 @@ stat = make_stat(
|
||||
"channels_dir": _dir_stat,
|
||||
"members_dir": _dir_stat,
|
||||
"channel": _channel_stat,
|
||||
"member": entry_stat("user_id", FileType.JSON),
|
||||
"member": entry_stat("user_id", ContentType.JSON),
|
||||
"files": _dir_stat,
|
||||
"file_blob": _file_blob_stat,
|
||||
},
|
||||
|
||||
@@ -58,6 +58,7 @@ async def stat(accessor: DiskAccessor,
|
||||
size=st.st_size,
|
||||
modified=modified,
|
||||
fingerprint=modified,
|
||||
type=guess_type(p.name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(p.name),
|
||||
mode=st.st_mode & 0o7777,
|
||||
atime=epoch_to_iso(st.st_atime))
|
||||
|
||||
@@ -45,7 +45,8 @@ def _stat_from_entry(entry: dict[str, Any]) -> FileStat:
|
||||
return FileStat(
|
||||
name=name,
|
||||
size=size if isinstance(size, int) else None,
|
||||
type=guess_type(name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(name),
|
||||
modified=modified,
|
||||
fingerprint=modified or None,
|
||||
extra={
|
||||
@@ -111,7 +112,8 @@ async def stat(
|
||||
return FileStat(
|
||||
name=result.entry.vfs_name or result.entry.name,
|
||||
size=result.entry.size,
|
||||
type=guess_type(result.entry.vfs_name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(result.entry.vfs_name),
|
||||
modified=result.entry.remote_time,
|
||||
fingerprint=result.entry.remote_time or None,
|
||||
extra={
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from mirage.core.hierarchy.codec import DATE, Codec
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
EMAIL_JSON = Codec(suffix=".email.json")
|
||||
|
||||
@@ -32,7 +32,7 @@ SCOPES = (
|
||||
Scope(kind="message",
|
||||
segments=_DAY + (Slot("message", EMAIL_JSON, id_key="uid"), ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="attachment_dir",
|
||||
segments=_DAY + (Slot("attachment_dir", id_key="uid"), )),
|
||||
Scope(kind="attachment",
|
||||
|
||||
@@ -17,7 +17,7 @@ from mirage.core.email.readdir import readdir
|
||||
from mirage.core.email.scope import detect_scope
|
||||
from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.filetype import guess_type
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ def _message_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
size=entry.size,
|
||||
extra={"uid": entry.id},
|
||||
)
|
||||
@@ -49,7 +50,8 @@ def _attachment_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=guess_type(entry.vfs_name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(entry.vfs_name),
|
||||
size=entry.size,
|
||||
extra={"attachment_id": entry.id},
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ from mirage.core.gcal.day import valid_day
|
||||
from mirage.core.hierarchy.codec import Codec
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.resource.gcal.event_entry import EVENT_SUFFIX
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
|
||||
def is_event_name(text: str) -> bool:
|
||||
@@ -50,12 +50,12 @@ SCOPES = (
|
||||
Scope(kind="calendar_json",
|
||||
segments=_CAL + ("calendar.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="day", segments=_DAY),
|
||||
Scope(kind="event",
|
||||
segments=_DAY + (Slot("event", EVENT_NAME), ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.gcal.scope import detect_scope
|
||||
from mirage.core.hierarchy.probe import resolve_entry
|
||||
from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ def _file_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
modified=entry.remote_time,
|
||||
size=entry.size,
|
||||
extra={
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from mirage.core.gdocs.constants import FILE_NAME
|
||||
from mirage.core.google.constants import CORPUS
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
# One description of the tree: readdir, stat, read and unlink all classify
|
||||
# through it, so the file surface and the write surface cannot disagree
|
||||
@@ -26,7 +26,7 @@ SCOPES = (
|
||||
segments=(Slot("corpus",
|
||||
CORPUS), Slot("name", FILE_NAME, id_key="file_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -17,14 +17,15 @@ from mirage.core.gdocs.readdir import readdir
|
||||
from mirage.core.gdocs.scope import detect_scope
|
||||
from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _file_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
modified=entry.remote_time,
|
||||
size=entry.size,
|
||||
extra={
|
||||
|
||||
@@ -62,7 +62,8 @@ async def stat_from_api(accessor: GDriveAccessor, key: str,
|
||||
return FileStat(
|
||||
name=vfs_name,
|
||||
size=size,
|
||||
type=guess_type(vfs_name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(vfs_name),
|
||||
modified=modified,
|
||||
fingerprint=modified or None,
|
||||
extra={
|
||||
@@ -107,7 +108,8 @@ async def stat(
|
||||
return FileStat(
|
||||
name=entry.vfs_name or entry.name,
|
||||
size=entry.size,
|
||||
type=guess_type(entry.vfs_name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(entry.vfs_name),
|
||||
modified=entry.remote_time,
|
||||
fingerprint=entry.remote_time or None,
|
||||
extra={
|
||||
|
||||
@@ -59,7 +59,8 @@ async def stat(
|
||||
return FileStat(
|
||||
name=result.entry.name,
|
||||
size=result.entry.size,
|
||||
type=guess_type(result.entry.name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(result.entry.name),
|
||||
fingerprint=result.entry.id,
|
||||
extra={"sha": result.entry.id},
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from mirage.core.hierarchy.codec import DATE, Codec
|
||||
from mirage.core.hierarchy.scope import ROOT, Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
GMAIL_JSON = Codec(suffix=".gmail.json")
|
||||
|
||||
@@ -32,7 +32,7 @@ SCOPES = (
|
||||
Scope(kind="message",
|
||||
segments=_DAY + (Slot("message", GMAIL_JSON, id_key="message_id"), ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="attachment_dir",
|
||||
segments=_DAY + (Slot("attachment_dir", id_key="message_id"), )),
|
||||
Scope(kind="attachment",
|
||||
|
||||
@@ -19,7 +19,7 @@ from mirage.core.gmail.scope import detect_scope
|
||||
from mirage.core.hierarchy.probe import resolve_entry
|
||||
from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
from mirage.utils.filetype import guess_type
|
||||
from mirage.utils.key_prefix import mount_key, mount_prefix_of
|
||||
@@ -38,7 +38,8 @@ def _message_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
size=entry.size,
|
||||
extra={
|
||||
"message_id": entry.id,
|
||||
@@ -60,7 +61,8 @@ def _attachment_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=guess_type(entry.vfs_name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(entry.vfs_name),
|
||||
size=entry.size,
|
||||
extra={"attachment_id": entry.id},
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from mirage.core.google.constants import CORPUS
|
||||
from mirage.core.gsheets.constants import FILE_NAME
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
# One description of the tree: readdir, stat, read and unlink all classify
|
||||
# through it, so the file surface and the write surface cannot disagree
|
||||
@@ -26,7 +26,7 @@ SCOPES = (
|
||||
segments=(Slot("corpus",
|
||||
CORPUS), Slot("name", FILE_NAME, id_key="file_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -17,14 +17,15 @@ from mirage.core.gsheets.readdir import readdir
|
||||
from mirage.core.gsheets.scope import detect_scope
|
||||
from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _file_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
modified=entry.remote_time,
|
||||
size=entry.size,
|
||||
extra={
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from mirage.core.google.constants import CORPUS
|
||||
from mirage.core.gslides.constants import FILE_NAME
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
# One description of the tree: readdir, stat, read and unlink all classify
|
||||
# through it, so the file surface and the write surface cannot disagree
|
||||
@@ -26,7 +26,7 @@ SCOPES = (
|
||||
segments=(Slot("corpus",
|
||||
CORPUS), Slot("name", FILE_NAME, id_key="file_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -17,14 +17,15 @@ from mirage.core.gslides.readdir import readdir
|
||||
from mirage.core.gslides.scope import detect_scope
|
||||
from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _file_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
modified=entry.remote_time,
|
||||
size=entry.size,
|
||||
extra={
|
||||
|
||||
@@ -16,7 +16,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from mirage.core.hierarchy.codec import RAW, Codec
|
||||
from mirage.types import FileType, PathSpec
|
||||
from mirage.types import ContentType, PathSpec
|
||||
|
||||
ROOT = "root"
|
||||
INVALID = "invalid"
|
||||
@@ -63,7 +63,7 @@ class Scope:
|
||||
slots.
|
||||
leaf (bool): whether the position is a file rather than a
|
||||
directory.
|
||||
filetype (FileType | None): rendered type of a leaf; None on
|
||||
filetype (ContentType | None): rendered type of a leaf; None on
|
||||
directories.
|
||||
probed (bool): whether stat must prove existence (parent listing
|
||||
by default); False for positions that exist by construction,
|
||||
@@ -72,7 +72,7 @@ class Scope:
|
||||
kind: str
|
||||
segments: tuple[Segment, ...]
|
||||
leaf: bool = False
|
||||
filetype: FileType | None = None
|
||||
filetype: ContentType | None = None
|
||||
probed: bool = True
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from mirage.core.hierarchy.probe import (A, ReaddirFn, assert_listed,
|
||||
listed_size, resolve_entry)
|
||||
from mirage.core.hierarchy.readdir import Guard
|
||||
from mirage.core.hierarchy.scope import ROOT, DetectFn, ScopeMatch
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
|
||||
ExtraFn = Callable[[ScopeMatch], dict[str, str]]
|
||||
@@ -29,23 +29,36 @@ StatHook = Callable[[A, ScopeMatch, PathSpec, IndexCacheStore],
|
||||
EntryStatFn = Callable[[ScopeMatch, PathSpec, IndexEntry], FileStat]
|
||||
|
||||
|
||||
def entry_stat(id_field: str, filetype: FileType) -> EntryStatFn:
|
||||
def entry_stat(id_field: str, filetype: ContentType | FileType) -> EntryStatFn:
|
||||
"""The shape most id-addressed nodes share, keyed by an id field.
|
||||
|
||||
Name from the entry's ``vfs_name``, size and modified straight off
|
||||
the listing, and the entry's id under ``id_field`` in ``extra``. A
|
||||
kind whose shape differs writes its own ``EntryStatFn`` instead.
|
||||
|
||||
``filetype`` is the node's kind: a ``FileType`` for a non-regular
|
||||
node (a directory entry, e.g. a linear team or trello board) or a
|
||||
``ContentType`` for a regular file, whose node kind is then FILE.
|
||||
|
||||
Args:
|
||||
id_field (str): the ``extra`` key the entry's id rides under.
|
||||
filetype (FileType): the node's rendered type.
|
||||
filetype (ContentType | FileType): the node's kind.
|
||||
"""
|
||||
|
||||
def build(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
if isinstance(filetype, FileType):
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=filetype,
|
||||
size=entry.size,
|
||||
modified=entry.remote_time or None,
|
||||
extra={id_field: entry.id},
|
||||
)
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=filetype,
|
||||
type=FileType.FILE,
|
||||
content=filetype,
|
||||
size=entry.size,
|
||||
modified=entry.remote_time or None,
|
||||
extra={id_field: entry.id},
|
||||
@@ -129,8 +142,9 @@ def make_stat(
|
||||
return FileStat(name=name, type=FileType.DIRECTORY, extra=extra)
|
||||
return FileStat(
|
||||
name=name,
|
||||
type=scope.filetype
|
||||
if scope.filetype is not None else FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=scope.filetype
|
||||
if scope.filetype is not None else ContentType.JSON,
|
||||
size=await listed_size(index, path),
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from mirage.accessor.history import HistoryAccessor
|
||||
from mirage.cache.index import NULL_INDEX, IndexCacheStore
|
||||
from mirage.core.history.read import read
|
||||
from mirage.types import FileStat, PathSpec
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.utils.filetype import guess_type
|
||||
|
||||
|
||||
@@ -37,5 +37,6 @@ async def stat(accessor: HistoryAccessor,
|
||||
name=".bash_history",
|
||||
size=len(data),
|
||||
modified=None,
|
||||
type=guess_type(".bash_history"),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(".bash_history"),
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from mirage.core.hierarchy.codec import Codec
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.core.jaeger.client import is_trace_id
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
OPERATIONS_FILE = "operations.json"
|
||||
TOP_LEVEL_DIRS = ["services"]
|
||||
@@ -33,13 +33,13 @@ SCOPES = (
|
||||
Scope(kind="operations",
|
||||
segments=("services", Slot("service"), OPERATIONS_FILE),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="traces", segments=("services", Slot("service"), "traces")),
|
||||
Scope(kind="trace",
|
||||
segments=("services", Slot("service"), "traces",
|
||||
Slot("trace_id", TRACE_FILE)),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -18,7 +18,7 @@ from mirage.core.hierarchy.codec import Codec
|
||||
from mirage.core.hierarchy.scope import (DetectFn, Scope, ScopeMatch, Segment,
|
||||
Slot, make_detect_scope)
|
||||
from mirage.resource.lancedb.config import LanceDBConfig
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
from mirage.utils.filetype import image_type_for_extension
|
||||
|
||||
CARD = Codec(suffix=".md")
|
||||
@@ -51,7 +51,7 @@ def scopes_for(config: LanceDBConfig) -> tuple[Scope, ...]:
|
||||
Scope(kind="row_card",
|
||||
segments=full + (Slot("row_id", CARD), ),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT))
|
||||
filetype=ContentType.TEXT))
|
||||
if config.blob_column:
|
||||
blob = Codec(suffix="." + config.blob_ext)
|
||||
scopes.append(
|
||||
|
||||
@@ -24,7 +24,7 @@ from mirage.core.lancedb.query import table_exists
|
||||
from mirage.core.lancedb.read import read
|
||||
from mirage.core.lancedb.readdir import readdir_for
|
||||
from mirage.core.lancedb.scope import detect_for, table_of
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
from mirage.utils.filetype import image_type_for_extension
|
||||
|
||||
@@ -48,16 +48,20 @@ async def _stat_row(accessor: LanceDBAccessor, match: ScopeMatch,
|
||||
if match.kind == "row_blob":
|
||||
file_type = image_type_for_extension(config.blob_ext)
|
||||
else:
|
||||
file_type = FileType.TEXT
|
||||
file_type = ContentType.TEXT
|
||||
# The row-dir readdir seeds exact card sizes; blob entries and a cold
|
||||
# index fall back to rendering the row, so the size is exact either way.
|
||||
lookup = await index.get(path.virtual.rstrip("/"))
|
||||
if lookup.entry is not None and lookup.entry.size is not None:
|
||||
return FileStat(name=_name_of(path),
|
||||
size=lookup.entry.size,
|
||||
type=file_type)
|
||||
type=FileType.FILE,
|
||||
content=file_type)
|
||||
data = await read(accessor, path, index)
|
||||
return FileStat(name=_name_of(path), size=len(data), type=file_type)
|
||||
return FileStat(name=_name_of(path),
|
||||
size=len(data),
|
||||
type=FileType.FILE,
|
||||
content=file_type)
|
||||
|
||||
|
||||
GUARDS: dict[str, Guard[LanceDBAccessor]] = {"group": _table_guard}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from mirage.core.hierarchy.codec import INT_JSON, JSON_NAME, JSONL_NAME
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
TOP_LEVEL_DIRS = ["traces", "sessions", "prompts", "datasets"]
|
||||
|
||||
@@ -27,14 +27,14 @@ SCOPES = (
|
||||
Scope(kind="trace",
|
||||
segments=("traces", Slot("trace_id", JSON_NAME)),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="sessions", segments=("sessions", ), probed=False),
|
||||
Scope(kind="session", segments=("sessions", Slot("session_id"))),
|
||||
Scope(kind="session_trace",
|
||||
segments=("sessions", Slot("session_id"),
|
||||
Slot("trace_id", JSON_NAME)),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="prompts", segments=("prompts", ), probed=False),
|
||||
Scope(kind="prompt", segments=("prompts", Slot("prompt_name"))),
|
||||
# A version that is not a plain ASCII integer cannot name a prompt
|
||||
@@ -43,19 +43,19 @@ SCOPES = (
|
||||
Scope(kind="prompt_version",
|
||||
segments=("prompts", Slot("prompt_name"), Slot("version", INT_JSON)),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="datasets", segments=("datasets", ), probed=False),
|
||||
Scope(kind="dataset", segments=("datasets", Slot("dataset_name"))),
|
||||
Scope(kind="dataset_items",
|
||||
segments=("datasets", Slot("dataset_name"), "items.jsonl"),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
Scope(kind="runs", segments=("datasets", Slot("dataset_name"), "runs")),
|
||||
Scope(kind="dataset_run",
|
||||
segments=("datasets", Slot("dataset_name"), "runs",
|
||||
Slot("run_name", JSONL_NAME)),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from mirage.core.hierarchy.codec import JSON_NAME
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
_TEAM = ("teams", Slot("team", id_key="team_id"))
|
||||
_ISSUE = _TEAM + ("issues", Slot("issue", id_key="issue_id"))
|
||||
@@ -30,41 +30,41 @@ SCOPES = (
|
||||
Scope(kind="team_json",
|
||||
segments=_TEAM + ("team.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="members", segments=_TEAM + ("members", )),
|
||||
Scope(kind="member",
|
||||
segments=_TEAM +
|
||||
("members", Slot("member", JSON_NAME, id_key="member_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="issues", segments=_TEAM + ("issues", )),
|
||||
Scope(kind="issue", segments=_ISSUE),
|
||||
Scope(kind="issue_json",
|
||||
segments=_ISSUE + ("issue.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="comments_jsonl",
|
||||
segments=_ISSUE + ("comments.jsonl", ),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
Scope(kind="projects", segments=_TEAM + ("projects", )),
|
||||
Scope(kind="project",
|
||||
segments=_TEAM +
|
||||
("projects", Slot("project", JSON_NAME, id_key="project_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="cycles", segments=_TEAM + ("cycles", )),
|
||||
Scope(kind="cycle",
|
||||
segments=_TEAM +
|
||||
("cycles", Slot("cycle", JSON_NAME, id_key="cycle_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="documents", segments=_TEAM + ("documents", )),
|
||||
Scope(kind="document",
|
||||
segments=_TEAM +
|
||||
("documents", Slot("document", JSON_NAME, id_key="document_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -15,20 +15,20 @@
|
||||
from mirage.core.hierarchy.stat import entry_stat, make_stat
|
||||
from mirage.core.linear.readdir import readdir
|
||||
from mirage.core.linear.scope import detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType, FileType
|
||||
|
||||
stat = make_stat(
|
||||
detect_scope,
|
||||
readdir,
|
||||
entry_stats={
|
||||
"team": entry_stat("team_id", FileType.DIRECTORY),
|
||||
"team_json": entry_stat("team_id", FileType.JSON),
|
||||
"member": entry_stat("user_id", FileType.JSON),
|
||||
"team_json": entry_stat("team_id", ContentType.JSON),
|
||||
"member": entry_stat("user_id", ContentType.JSON),
|
||||
"issue": entry_stat("issue_id", FileType.DIRECTORY),
|
||||
"issue_json": entry_stat("issue_id", FileType.JSON),
|
||||
"comments_jsonl": entry_stat("issue_id", FileType.TEXT),
|
||||
"project": entry_stat("project_id", FileType.JSON),
|
||||
"cycle": entry_stat("cycle_id", FileType.JSON),
|
||||
"document": entry_stat("document_id", FileType.JSON),
|
||||
"issue_json": entry_stat("issue_id", ContentType.JSON),
|
||||
"comments_jsonl": entry_stat("issue_id", ContentType.TEXT),
|
||||
"project": entry_stat("project_id", ContentType.JSON),
|
||||
"cycle": entry_stat("cycle_id", ContentType.JSON),
|
||||
"document": entry_stat("document_id", ContentType.JSON),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
|
||||
from mirage.core.hierarchy.codec import JSON_NAME
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
# The mount is one flat directory of memory files; which memories exist
|
||||
# is a function of the configured scope filter, not of the path.
|
||||
SCOPES = (Scope(kind="memory",
|
||||
segments=(Slot("memory_id", JSON_NAME), ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON), )
|
||||
filetype=ContentType.JSON), )
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -22,14 +22,15 @@ from mirage.core.mem0.client import get_memory
|
||||
from mirage.core.mem0.readdir import readdir
|
||||
from mirage.core.mem0.scope import detect_scope
|
||||
from mirage.core.render.json import json_bytes
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _file_stat(memory: dict[str, Any]) -> FileStat:
|
||||
body = json_bytes(memory)
|
||||
return FileStat(
|
||||
name=f"{memory['id']}.json",
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
size=len(body),
|
||||
modified=memory.get("updated_at") or memory.get("created_at"),
|
||||
extra={
|
||||
|
||||
@@ -16,7 +16,7 @@ from mirage.core.hierarchy.codec import Codec
|
||||
from mirage.core.hierarchy.scope import (Scope, ScopeMatch, Slot,
|
||||
make_detect_scope)
|
||||
from mirage.core.mongodb.types import KIND_DIR_NAMES, EntityKind
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
|
||||
def is_kind_dir(text: str) -> bool:
|
||||
@@ -38,7 +38,7 @@ SCOPES = (
|
||||
Scope(kind="database_json",
|
||||
segments=(Slot("database"), "database.json"),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
Scope(kind="kind_dir", segments=(Slot("database"), Slot("kind", KIND))),
|
||||
Scope(kind="entity",
|
||||
segments=(Slot("database"), Slot("kind", KIND), Slot("name"))),
|
||||
@@ -46,12 +46,12 @@ SCOPES = (
|
||||
segments=(Slot("database"), Slot("kind",
|
||||
KIND), Slot("name"), "schema.json"),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
Scope(kind="documents",
|
||||
segments=(Slot("database"), Slot("kind", KIND), Slot("name"),
|
||||
"documents.jsonl"),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -22,7 +22,7 @@ from mirage.core.mongodb.client import count_documents, get_indexes, is_view
|
||||
from mirage.core.mongodb.readdir import database_guard, entity_guard, readdir
|
||||
from mirage.core.mongodb.scope import detect_scope, entity_kind
|
||||
from mirage.core.mongodb.types import EntityKind
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _database_extra(match: ScopeMatch) -> dict[str, str]:
|
||||
@@ -80,7 +80,8 @@ async def _documents_stat(accessor: MongoDBAccessor, match: ScopeMatch,
|
||||
} for idx in indexes]
|
||||
return FileStat(
|
||||
name="documents.jsonl",
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
extra={
|
||||
"database": database,
|
||||
"name": name,
|
||||
|
||||
@@ -278,7 +278,8 @@ def entry_stat(item: dict[str, Any]) -> FileStat:
|
||||
name=name,
|
||||
size=item.get("size"),
|
||||
modified=item.get("lastModifiedDateTime"),
|
||||
type=guess_type(name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(name),
|
||||
fingerprint=item.get("cTag"),
|
||||
extra={
|
||||
"id": item.get("id"),
|
||||
@@ -645,7 +646,8 @@ async def stat_item(config: MsGraphConfig, loc: DriveLoc, virtual: str,
|
||||
return FileStat(name=entry.name,
|
||||
size=entry.size,
|
||||
modified=entry.remote_time or None,
|
||||
type=guess_type(entry.name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(entry.name),
|
||||
extra=dict(entry.extra))
|
||||
parent = virtual_key.rsplit("/", 1)[0] or "/"
|
||||
parent_listing = await index.list_dir(parent)
|
||||
|
||||
@@ -31,7 +31,8 @@ async def stat(accessor: NextcloudAccessor,
|
||||
return FileStat(name=entry.name,
|
||||
size=entry.size,
|
||||
modified=entry.remote_time or None,
|
||||
type=guess_type(entry.name))
|
||||
type=FileType.FILE,
|
||||
content=guess_type(entry.name))
|
||||
parent = virtual_key.rsplit("/", 1)[0] or "/"
|
||||
parent_listing = await index.list_dir(parent)
|
||||
if parent_listing.entries is not None:
|
||||
@@ -48,7 +49,8 @@ async def stat(accessor: NextcloudAccessor,
|
||||
name=stripped.rsplit("/", 1)[-1],
|
||||
size=md.content_length,
|
||||
modified=modified,
|
||||
type=guess_type(raw),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(raw),
|
||||
fingerprint=md.etag,
|
||||
extra={"etag": md.etag} if md.etag else {},
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
# A page tree nests arbitrarily, so the page level is one VARIADIC slot:
|
||||
# `pages/a__1/b__2` is a page at any depth, and the slots hold the DEEPEST
|
||||
@@ -34,22 +34,22 @@ SCOPES = (
|
||||
Scope(kind="page_json",
|
||||
segments=("pages", _PAGE, "page.json"),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="page", segments=("pages", _PAGE)),
|
||||
Scope(kind="database_json",
|
||||
segments=_DB + ("database.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="database", segments=_DB),
|
||||
Scope(kind="data_source_json",
|
||||
segments=_DS + ("data_source.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="data_source", segments=_DS),
|
||||
Scope(kind="page_json",
|
||||
segments=_DS + (_PAGE, "page.json"),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="page", segments=_DS + (_PAGE, )),
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.core.notion.readdir import readdir
|
||||
from mirage.core.notion.scope import detect_scope
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _page_stat(match: ScopeMatch, path: PathSpec,
|
||||
@@ -32,7 +32,10 @@ def _page_stat(match: ScopeMatch, path: PathSpec,
|
||||
|
||||
def _page_json_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(name=entry.vfs_name, type=FileType.JSON, size=entry.size)
|
||||
return FileStat(name=entry.vfs_name,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
size=entry.size)
|
||||
|
||||
|
||||
def _database_stat(match: ScopeMatch, path: PathSpec,
|
||||
@@ -49,7 +52,8 @@ def _database_json_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
size=entry.size,
|
||||
extra={"database_id": match.slots["database_id"]},
|
||||
)
|
||||
@@ -69,7 +73,8 @@ def _data_source_json_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
size=entry.size,
|
||||
extra={"data_source_id": match.slots["data_source_id"]},
|
||||
)
|
||||
|
||||
@@ -68,7 +68,8 @@ def make_stat(driver: ObjectStoreDriver[A, C]) -> StatFn[A]:
|
||||
name=entry.name,
|
||||
size=entry.size,
|
||||
modified=entry.remote_time or None,
|
||||
type=guess_type(entry.name),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(entry.name),
|
||||
)
|
||||
# If the parent directory was already listed by readdir() but
|
||||
# this path is not among its children, it does not exist.
|
||||
@@ -94,7 +95,8 @@ def make_stat(driver: ObjectStoreDriver[A, C]) -> StatFn[A]:
|
||||
name=path.rstrip("/").rsplit("/", 1)[-1],
|
||||
size=meta.size,
|
||||
modified=meta.modified,
|
||||
type=guess_type(path),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(path),
|
||||
fingerprint=meta.fingerprint,
|
||||
revision=meta.revision,
|
||||
extra=dict(meta.extra),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from mirage.core.hierarchy.codec import Codec
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
ENTITY_FILES = ("schema.json", "semantic.json", "rows.jsonl")
|
||||
|
||||
@@ -39,7 +39,7 @@ SCOPES = (
|
||||
Scope(kind="database_json",
|
||||
segments=("database.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON,
|
||||
filetype=ContentType.JSON,
|
||||
probed=False),
|
||||
Scope(kind="schema", segments=(Slot("schema"), )),
|
||||
Scope(kind="kind", segments=(Slot("schema"), Slot("kind", KIND))),
|
||||
@@ -49,17 +49,17 @@ SCOPES = (
|
||||
segments=(Slot("schema"), Slot("kind",
|
||||
KIND), Slot("entity"), "schema.json"),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="entity_semantic",
|
||||
segments=(Slot("schema"), Slot("kind", KIND), Slot("entity"),
|
||||
"semantic.json"),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="entity_rows",
|
||||
segments=(Slot("schema"), Slot("kind",
|
||||
KIND), Slot("entity"), "rows.jsonl"),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -23,7 +23,7 @@ from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.core.postgres import client
|
||||
from mirage.core.postgres.readdir import readdir
|
||||
from mirage.core.postgres.scope import detect_scope
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ async def _rows_stat(accessor: PostgresAccessor, match: ScopeMatch,
|
||||
# see the CLAUDE.md FUSE rules). The storage size remains in extra.
|
||||
return FileStat(
|
||||
name="rows.jsonl",
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=None,
|
||||
fingerprint=fingerprint,
|
||||
extra={
|
||||
|
||||
@@ -18,7 +18,7 @@ from mirage.core.hierarchy.codec import JSON_NAME, Codec
|
||||
from mirage.core.hierarchy.scope import (DetectFn, Scope, ScopeMatch, Segment,
|
||||
Slot, make_detect_scope)
|
||||
from mirage.resource.qdrant.config import QdrantConfig
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
from mirage.utils.filetype import image_type_for_extension
|
||||
|
||||
TXT = Codec(suffix=".txt")
|
||||
@@ -53,13 +53,13 @@ def scopes_for(config: QdrantConfig) -> tuple[Scope, ...]:
|
||||
Scope(kind="row_json",
|
||||
segments=full + (Slot("row_id", JSON_NAME), ),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT))
|
||||
filetype=ContentType.TEXT))
|
||||
if config.text_field:
|
||||
scopes.append(
|
||||
Scope(kind="row_text",
|
||||
segments=full + (Slot("row_id", TXT), ),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT))
|
||||
filetype=ContentType.TEXT))
|
||||
if config.blob_field:
|
||||
blob = Codec(suffix="." + config.blob_ext)
|
||||
scopes.append(
|
||||
|
||||
@@ -24,7 +24,7 @@ from mirage.core.qdrant.query import table_exists
|
||||
from mirage.core.qdrant.read import read
|
||||
from mirage.core.qdrant.readdir import readdir_for
|
||||
from mirage.core.qdrant.scope import detect_for, table_of
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
from mirage.utils.filetype import image_type_for_extension
|
||||
|
||||
@@ -48,16 +48,20 @@ async def _stat_row(accessor: QdrantAccessor, match: ScopeMatch,
|
||||
if match.kind == "row_blob":
|
||||
file_type = image_type_for_extension(config.blob_ext)
|
||||
else:
|
||||
file_type = FileType.TEXT
|
||||
file_type = ContentType.TEXT
|
||||
# The row-dir readdir seeds exact rendered sizes; a cold index falls
|
||||
# back to rendering the row, so the size is exact either way.
|
||||
lookup = await index.get(path.virtual.rstrip("/"))
|
||||
if lookup.entry is not None and lookup.entry.size is not None:
|
||||
return FileStat(name=_name_of(path),
|
||||
size=lookup.entry.size,
|
||||
type=file_type)
|
||||
type=FileType.FILE,
|
||||
content=file_type)
|
||||
data = await read(accessor, path, index)
|
||||
return FileStat(name=_name_of(path), size=len(data), type=file_type)
|
||||
return FileStat(name=_name_of(path),
|
||||
size=len(data),
|
||||
type=FileType.FILE,
|
||||
content=file_type)
|
||||
|
||||
|
||||
GUARDS: dict[str, Guard[QdrantAccessor]] = {"group": _table_guard}
|
||||
|
||||
@@ -45,7 +45,8 @@ async def stat(accessor: RAMAccessor,
|
||||
name=p.rsplit("/", 1)[-1],
|
||||
size=len(data),
|
||||
modified=store.modified.get(p),
|
||||
type=guess_type(p),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(p),
|
||||
mode=attrs.get("mode"),
|
||||
uid=attrs.get("uid"),
|
||||
gid=attrs.get("gid"),
|
||||
|
||||
@@ -63,7 +63,8 @@ async def stat(
|
||||
name=p.rsplit("/", 1)[-1],
|
||||
size=size,
|
||||
modified=await store.get_modified(p),
|
||||
type=guess_type(p),
|
||||
type=FileType.FILE,
|
||||
content=guess_type(p),
|
||||
mode=attrs.get("mode"),
|
||||
uid=attrs.get("uid"),
|
||||
gid=attrs.get("gid"),
|
||||
|
||||
@@ -17,7 +17,7 @@ from dataclasses import dataclass
|
||||
from mirage.core.hierarchy.codec import DATE, JSON_NAME, Codec
|
||||
from mirage.core.hierarchy.scope import (ROOT, Scope, ScopeMatch, Slot,
|
||||
make_detect_scope)
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
|
||||
def is_container(text: str) -> bool:
|
||||
@@ -47,13 +47,13 @@ SCOPES = (
|
||||
Scope(kind="user",
|
||||
segments=("users", Slot("user", JSON_NAME, id_key="user_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="channel", segments=_CHANNEL),
|
||||
Scope(kind="day", segments=_DAY),
|
||||
Scope(kind="messages",
|
||||
segments=_DAY + ("chat.jsonl", ),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
Scope(kind="files", segments=_DAY + ("files", )),
|
||||
Scope(kind="file_blob", segments=_DAY + ("files", Slot("blob")),
|
||||
leaf=True),
|
||||
|
||||
@@ -20,7 +20,7 @@ from mirage.core.hierarchy.stat import make_stat
|
||||
from mirage.core.slack.readdir import readdir
|
||||
from mirage.core.slack.scope import detect_scope
|
||||
from mirage.core.timeutil import epoch_to_iso
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
from mirage.utils.filetype import filetype_from_mimetype
|
||||
from mirage.utils.key_prefix import mount_key, mount_prefix_of
|
||||
@@ -52,7 +52,8 @@ def _user_stat(match: ScopeMatch, path: PathSpec,
|
||||
entry: IndexEntry) -> FileStat:
|
||||
return FileStat(
|
||||
name=entry.vfs_name or entry.name,
|
||||
type=FileType.JSON,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
size=entry.size,
|
||||
extra={"user_id": entry.id},
|
||||
)
|
||||
@@ -68,7 +69,8 @@ def _file_blob_stat(match: ScopeMatch, path: PathSpec,
|
||||
mimetype = entry.extra.get("mimetype", "")
|
||||
return FileStat(
|
||||
name=entry.vfs_name or entry.name,
|
||||
type=filetype_from_mimetype(mimetype),
|
||||
type=FileType.FILE,
|
||||
content=filetype_from_mimetype(mimetype),
|
||||
size=entry.size,
|
||||
modified=_slack_modified(entry.remote_time),
|
||||
extra={"file_id": entry.id},
|
||||
@@ -125,7 +127,10 @@ def _chat_stat(match: ScopeMatch, path: PathSpec,
|
||||
# A denied or empty day lists no chat.jsonl, and the kit reports the
|
||||
# absent entry as ENOENT: slack does not fabricate a sizeless file
|
||||
# for a sealed day (discord deliberately does; see its override).
|
||||
return FileStat(name="chat.jsonl", type=FileType.TEXT, size=entry.size)
|
||||
return FileStat(name="chat.jsonl",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=entry.size)
|
||||
|
||||
|
||||
stat = make_stat(
|
||||
|
||||
@@ -49,7 +49,8 @@ async def stat(accessor: SSHAccessor,
|
||||
size=attrs.size or 0,
|
||||
modified=mod_str,
|
||||
fingerprint=mod_str or None,
|
||||
type=FileType.DIRECTORY if is_dir else guess_type(path),
|
||||
type=FileType.DIRECTORY if is_dir else FileType.FILE,
|
||||
content=None if is_dir else guess_type(path),
|
||||
mode=(attrs.permissions
|
||||
& 0o7777 if attrs.permissions is not None else None),
|
||||
atime=(epoch_to_iso(attrs.atime)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from mirage.core.hierarchy.codec import JSON_NAME
|
||||
from mirage.core.hierarchy.scope import Scope, Slot, make_detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
_WS = ("workspaces", Slot("workspace", id_key="workspace_id"))
|
||||
_BOARD = _WS + ("boards", Slot("board", id_key="board_id"))
|
||||
@@ -32,41 +32,41 @@ SCOPES = (
|
||||
Scope(kind="workspace_json",
|
||||
segments=_WS + ("workspace.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="boards", segments=_WS + ("boards", )),
|
||||
Scope(kind="board", segments=_BOARD),
|
||||
Scope(kind="board_json",
|
||||
segments=_BOARD + ("board.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="members", segments=_BOARD + ("members", )),
|
||||
Scope(kind="member",
|
||||
segments=_BOARD +
|
||||
("members", Slot("member", JSON_NAME, id_key="member_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="labels", segments=_BOARD + ("labels", )),
|
||||
Scope(kind="label",
|
||||
segments=_BOARD +
|
||||
("labels", Slot("label", JSON_NAME, id_key="label_id")),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="lists", segments=_BOARD + ("lists", )),
|
||||
Scope(kind="list", segments=_LIST),
|
||||
Scope(kind="list_json",
|
||||
segments=_LIST + ("list.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="cards", segments=_LIST + ("cards", )),
|
||||
Scope(kind="card", segments=_CARD),
|
||||
Scope(kind="card_json",
|
||||
segments=_CARD + ("card.json", ),
|
||||
leaf=True,
|
||||
filetype=FileType.JSON),
|
||||
filetype=ContentType.JSON),
|
||||
Scope(kind="comments_jsonl",
|
||||
segments=_CARD + ("comments.jsonl", ),
|
||||
leaf=True,
|
||||
filetype=FileType.TEXT),
|
||||
filetype=ContentType.TEXT),
|
||||
)
|
||||
|
||||
detect_scope = make_detect_scope(SCOPES)
|
||||
|
||||
@@ -15,22 +15,22 @@
|
||||
from mirage.core.hierarchy.stat import entry_stat, make_stat
|
||||
from mirage.core.trello.readdir import readdir
|
||||
from mirage.core.trello.scope import detect_scope
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType, FileType
|
||||
|
||||
stat = make_stat(
|
||||
detect_scope,
|
||||
readdir,
|
||||
entry_stats={
|
||||
"workspace": entry_stat("workspace_id", FileType.DIRECTORY),
|
||||
"workspace_json": entry_stat("workspace_id", FileType.JSON),
|
||||
"workspace_json": entry_stat("workspace_id", ContentType.JSON),
|
||||
"board": entry_stat("board_id", FileType.DIRECTORY),
|
||||
"board_json": entry_stat("board_id", FileType.JSON),
|
||||
"member": entry_stat("member_id", FileType.JSON),
|
||||
"label": entry_stat("label_id", FileType.JSON),
|
||||
"board_json": entry_stat("board_id", ContentType.JSON),
|
||||
"member": entry_stat("member_id", ContentType.JSON),
|
||||
"label": entry_stat("label_id", ContentType.JSON),
|
||||
"list": entry_stat("list_id", FileType.DIRECTORY),
|
||||
"list_json": entry_stat("list_id", FileType.JSON),
|
||||
"list_json": entry_stat("list_id", ContentType.JSON),
|
||||
"card": entry_stat("card_id", FileType.DIRECTORY),
|
||||
"card_json": entry_stat("card_id", FileType.JSON),
|
||||
"comments_jsonl": entry_stat("card_id", FileType.TEXT),
|
||||
"card_json": entry_stat("card_id", ContentType.JSON),
|
||||
"comments_jsonl": entry_stat("card_id", ContentType.TEXT),
|
||||
},
|
||||
)
|
||||
|
||||
+39
-2
@@ -18,7 +18,8 @@ from datetime import datetime
|
||||
from enum import Enum, StrEnum
|
||||
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Protocol, TypeAlias
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt
|
||||
from pydantic import (BaseModel, ConfigDict, Field, NonNegativeInt,
|
||||
model_validator)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import aiohttp
|
||||
@@ -62,8 +63,34 @@ class LsSortBy(str, Enum):
|
||||
|
||||
|
||||
class FileType(str, Enum):
|
||||
"""POSIX file type (the `st_mode` kind), the switch behavior branches on.
|
||||
|
||||
One per entry, always present. Directory and symlink are their own
|
||||
kinds; every regular file is FILE and carries its content shape on
|
||||
FileStat.content. Distinct from ContentType, which is only a
|
||||
rendering hint for a FILE.
|
||||
|
||||
The full POSIX set is enumerated so the model is comprehensive. Only
|
||||
DIRECTORY, FILE and SYMLINK are produced today; CHAR_DEVICE,
|
||||
BLOCK_DEVICE, FIFO and SOCKET are declared but not yet emitted, and
|
||||
the render/derivation tables (find letter, st_mode bits, ls char)
|
||||
grow a row for one the moment a backend starts producing it.
|
||||
"""
|
||||
DIRECTORY = "directory"
|
||||
FILE = "file"
|
||||
SYMLINK = "symlink"
|
||||
CHAR_DEVICE = "char_device"
|
||||
BLOCK_DEVICE = "block_device"
|
||||
FIFO = "fifo"
|
||||
SOCKET = "socket"
|
||||
|
||||
|
||||
class ContentType(str, Enum):
|
||||
"""A regular file's content shape: the rendering hint (file/ls color).
|
||||
|
||||
Only meaningful for a FILE; a directory or symlink carries none. Not
|
||||
a node kind -- nothing branches control flow on it.
|
||||
"""
|
||||
TEXT = "text"
|
||||
BINARY = "binary"
|
||||
JSON = "json"
|
||||
@@ -90,13 +117,23 @@ class FileStat(BaseModel):
|
||||
modified: str | None = None
|
||||
fingerprint: str | None = None
|
||||
revision: str | None = None
|
||||
type: FileType | None = None
|
||||
type: FileType
|
||||
content: ContentType | None = None
|
||||
mode: int | None = None
|
||||
uid: int | str | None = None
|
||||
gid: int | str | None = None
|
||||
atime: str | None = None
|
||||
extra: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _content_only_on_file(self) -> "FileStat":
|
||||
# content is a FILE's rendering hint; a directory or symlink has
|
||||
# none. None on a FILE means "unknown", which is allowed.
|
||||
if self.type is not FileType.FILE and self.content is not None:
|
||||
raise ValueError(f"content must be None for {self.type.value}, "
|
||||
f"got {self.content.value}")
|
||||
return self
|
||||
|
||||
|
||||
# Any value that survives a JSON round trip: what a decoded payload
|
||||
# holds, what jq evaluates over, what an API field hands back. Recursive
|
||||
|
||||
@@ -12,33 +12,33 @@
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.types import FileType
|
||||
from mirage.types import ContentType
|
||||
|
||||
EXTENSION_MAP: dict[str, FileType] = {
|
||||
"json": FileType.JSON,
|
||||
"jsonl": FileType.JSON,
|
||||
"csv": FileType.CSV,
|
||||
"tsv": FileType.CSV,
|
||||
"txt": FileType.TEXT,
|
||||
"md": FileType.TEXT,
|
||||
"log": 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,
|
||||
"gzip": FileType.GZIP,
|
||||
"pdf": FileType.PDF,
|
||||
EXTENSION_MAP: dict[str, ContentType] = {
|
||||
"json": ContentType.JSON,
|
||||
"jsonl": ContentType.JSON,
|
||||
"csv": ContentType.CSV,
|
||||
"tsv": ContentType.CSV,
|
||||
"txt": ContentType.TEXT,
|
||||
"md": ContentType.TEXT,
|
||||
"log": ContentType.TEXT,
|
||||
"py": ContentType.TEXT,
|
||||
"js": ContentType.TEXT,
|
||||
"ts": ContentType.TEXT,
|
||||
"yaml": ContentType.TEXT,
|
||||
"yml": ContentType.TEXT,
|
||||
"toml": ContentType.TEXT,
|
||||
"png": ContentType.IMAGE_PNG,
|
||||
"jpg": ContentType.IMAGE_JPEG,
|
||||
"jpeg": ContentType.IMAGE_JPEG,
|
||||
"gif": ContentType.IMAGE_GIF,
|
||||
"zip": ContentType.ZIP,
|
||||
"gz": ContentType.GZIP,
|
||||
"gzip": ContentType.GZIP,
|
||||
"pdf": ContentType.PDF,
|
||||
}
|
||||
|
||||
DEFAULT_TYPE = FileType.BINARY
|
||||
DEFAULT_TYPE = ContentType.BINARY
|
||||
|
||||
# Extension-guessed like upstream mailers' mime_guess, as a deliberate
|
||||
# fixed subset: the stdlib mimetypes module consults platform tables,
|
||||
@@ -79,64 +79,64 @@ def mime_type_for(filename: str) -> str:
|
||||
return MIME_BY_EXTENSION.get(extension.lower(), OCTET_STREAM)
|
||||
|
||||
|
||||
_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,
|
||||
_MIMETYPE_MAP: dict[str, ContentType] = {
|
||||
"application/pdf": ContentType.PDF,
|
||||
"application/zip": ContentType.ZIP,
|
||||
"application/gzip": ContentType.GZIP,
|
||||
"application/json": ContentType.JSON,
|
||||
"image/png": ContentType.IMAGE_PNG,
|
||||
"image/jpeg": ContentType.IMAGE_JPEG,
|
||||
"image/gif": ContentType.IMAGE_GIF,
|
||||
"text/csv": ContentType.CSV,
|
||||
}
|
||||
|
||||
|
||||
def guess_type(path: str) -> FileType:
|
||||
def guess_type(path: str) -> ContentType:
|
||||
"""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.
|
||||
ContentType: 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)
|
||||
|
||||
|
||||
IMAGE_TYPE_BY_EXTENSION: dict[str, FileType] = {
|
||||
"png": FileType.IMAGE_PNG,
|
||||
"jpg": FileType.IMAGE_JPEG,
|
||||
"jpeg": FileType.IMAGE_JPEG,
|
||||
"gif": FileType.IMAGE_GIF,
|
||||
IMAGE_TYPE_BY_EXTENSION: dict[str, ContentType] = {
|
||||
"png": ContentType.IMAGE_PNG,
|
||||
"jpg": ContentType.IMAGE_JPEG,
|
||||
"jpeg": ContentType.IMAGE_JPEG,
|
||||
"gif": ContentType.IMAGE_GIF,
|
||||
}
|
||||
|
||||
|
||||
def image_type_for_extension(ext: str) -> FileType:
|
||||
"""Return the FileType for a bare image extension.
|
||||
def image_type_for_extension(ext: str) -> ContentType:
|
||||
"""Return the ContentType for a bare image extension.
|
||||
|
||||
Args:
|
||||
ext (str): extension without the dot (e.g. ``png``).
|
||||
|
||||
Returns:
|
||||
FileType: matched image type, or BINARY for anything else.
|
||||
ContentType: matched image type, or BINARY for anything else.
|
||||
"""
|
||||
return IMAGE_TYPE_BY_EXTENSION.get(ext.lower(), FileType.BINARY)
|
||||
return IMAGE_TYPE_BY_EXTENSION.get(ext.lower(), ContentType.BINARY)
|
||||
|
||||
|
||||
def filetype_from_mimetype(mime: str) -> FileType:
|
||||
"""Map a standard mimetype string to a FileType.
|
||||
def filetype_from_mimetype(mime: str) -> ContentType:
|
||||
"""Map a standard mimetype string to a ContentType.
|
||||
|
||||
Args:
|
||||
mime (str): mimetype string (e.g., "image/png", "application/pdf").
|
||||
|
||||
Returns:
|
||||
FileType: matched type, TEXT for any text/*, or BINARY default.
|
||||
ContentType: matched type, TEXT for any text/*, or BINARY default.
|
||||
"""
|
||||
if not mime:
|
||||
return FileType.BINARY
|
||||
return ContentType.BINARY
|
||||
if mime in _MIMETYPE_MAP:
|
||||
return _MIMETYPE_MAP[mime]
|
||||
if mime.startswith("text/"):
|
||||
return FileType.TEXT
|
||||
return FileType.BINARY
|
||||
return ContentType.TEXT
|
||||
return ContentType.BINARY
|
||||
|
||||
@@ -18,11 +18,11 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
import pytest
|
||||
|
||||
from mirage.agents.openai_agents.runner import MirageRunner
|
||||
from mirage.types import FileStat, FileType
|
||||
from mirage.types import ContentType, FileStat, FileType
|
||||
|
||||
|
||||
def _stat(file_type: FileType) -> FileStat:
|
||||
return FileStat(name="x", type=file_type)
|
||||
def _stat(file_type: ContentType) -> FileStat:
|
||||
return FileStat(name="x", type=FileType.FILE, content=file_type)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -36,7 +36,7 @@ def ws():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_image_inlines_base64(ws):
|
||||
ws.ops.stat.return_value = _stat(FileType.IMAGE_PNG)
|
||||
ws.ops.stat.return_value = _stat(ContentType.IMAGE_PNG)
|
||||
ws.ops.read.return_value = b"\x89PNG\r\n\x1a\n"
|
||||
runner = MirageRunner(ws)
|
||||
blocks = await runner.build_blocks("hi", ["/img.png"])
|
||||
@@ -48,7 +48,7 @@ async def test_build_blocks_image_inlines_base64(ws):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_pdf_uploads_to_files_api(ws):
|
||||
ws.ops.stat.return_value = _stat(FileType.PDF)
|
||||
ws.ops.stat.return_value = _stat(ContentType.PDF)
|
||||
ws.ops.read.return_value = b"%PDF-1.4 ..."
|
||||
fake_client = MagicMock()
|
||||
fake_client.files = MagicMock()
|
||||
@@ -66,7 +66,7 @@ async def test_build_blocks_pdf_uploads_to_files_api(ws):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_text_decoded_inline(ws):
|
||||
ws.ops.stat.return_value = _stat(FileType.TEXT)
|
||||
ws.ops.stat.return_value = _stat(ContentType.TEXT)
|
||||
ws.ops.read.return_value = b"hello world"
|
||||
runner = MirageRunner(ws)
|
||||
blocks = await runner.build_blocks("look", ["/notes.txt"])
|
||||
@@ -75,7 +75,7 @@ async def test_build_blocks_text_decoded_inline(ws):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_jpeg(ws):
|
||||
ws.ops.stat.return_value = _stat(FileType.IMAGE_JPEG)
|
||||
ws.ops.stat.return_value = _stat(ContentType.IMAGE_JPEG)
|
||||
ws.ops.read.return_value = b"\xff\xd8\xff..."
|
||||
runner = MirageRunner(ws)
|
||||
blocks = await runner.build_blocks("see", ["/photo.jpg"])
|
||||
@@ -85,7 +85,7 @@ async def test_build_blocks_jpeg(ws):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_multiple_paths_in_order(ws):
|
||||
types = [FileType.TEXT, FileType.IMAGE_PNG]
|
||||
types = [ContentType.TEXT, ContentType.IMAGE_PNG]
|
||||
bytes_seq = [b"first", b"\x89PNG\r\n\x1a\n"]
|
||||
|
||||
async def fake_stat(p):
|
||||
|
||||
@@ -21,12 +21,14 @@ from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.commands.builtin.box.pushdown import narrow_scope
|
||||
from mirage.core.box.client import BoxTokenManager
|
||||
from mirage.core.box.config import BoxConfig
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
_NGLOBALS = narrow_scope.__globals__
|
||||
|
||||
DIR_STAT = FileStat(name="data", type=FileType.DIRECTORY)
|
||||
FILE_STAT = FileStat(name="x.txt", type=FileType.TEXT)
|
||||
FILE_STAT = FileStat(name="x.txt",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
|
||||
def make_accessor(content_search: bool = True) -> BoxAccessor:
|
||||
|
||||
@@ -21,7 +21,7 @@ from mirage.commands.builtin.discord.rg import rg
|
||||
from mirage.commands.config import CommandOpts
|
||||
from mirage.commands.errors import UsageError
|
||||
from mirage.io.types import IOResult, materialize
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@@ -232,7 +232,8 @@ async def test_discord_grep_falls_back_when_native_raises():
|
||||
), patch(
|
||||
"mirage.commands.builtin.discord.grep._stat",
|
||||
new=AsyncMock(return_value=FileStat(name="2026-04-10.jsonl",
|
||||
type=FileType.TEXT)),
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)),
|
||||
):
|
||||
out, io = await grep(accessor, paths, ['hello'],
|
||||
CommandOpts(flags={'w': True}))
|
||||
@@ -297,7 +298,8 @@ async def test_discord_grep_multi_pattern_skips_native_search():
|
||||
), patch(
|
||||
"mirage.commands.builtin.discord.grep._stat",
|
||||
new=AsyncMock(return_value=FileStat(name="2026-04-10.jsonl",
|
||||
type=FileType.TEXT)),
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)),
|
||||
):
|
||||
_, io = await grep(accessor, paths, [],
|
||||
CommandOpts(flags={
|
||||
@@ -370,7 +372,8 @@ async def test_discord_rg_multi_pattern_skips_native_search():
|
||||
), patch(
|
||||
"mirage.commands.builtin.discord.rg._stat",
|
||||
new=AsyncMock(return_value=FileStat(name="2026-04-10.jsonl",
|
||||
type=FileType.TEXT)),
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)),
|
||||
):
|
||||
_, io = await rg(accessor, paths, [],
|
||||
CommandOpts(flags={
|
||||
@@ -424,8 +427,9 @@ async def test_discord_grep_file_blob_skips_native_search():
|
||||
new=AsyncMock(return_value=b"quarter,amount\n"),
|
||||
), patch(
|
||||
"mirage.commands.builtin.discord.grep._stat",
|
||||
new=AsyncMock(
|
||||
return_value=FileStat(name="img__A1.png", type=FileType.TEXT)),
|
||||
new=AsyncMock(return_value=FileStat(name="img__A1.png",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)),
|
||||
):
|
||||
out, io = await grep(accessor, paths, ['quarter'],
|
||||
CommandOpts(flags={'w': True}))
|
||||
|
||||
@@ -21,12 +21,14 @@ from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.commands.builtin.dropbox.pushdown import narrow_scope
|
||||
from mirage.core.dropbox.client import DropboxTokenManager
|
||||
from mirage.resource.dropbox.config import DropboxConfig
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
_NGLOBALS = narrow_scope.__globals__
|
||||
|
||||
DIR_STAT = FileStat(name="data", type=FileType.DIRECTORY)
|
||||
FILE_STAT = FileStat(name="x.txt", type=FileType.TEXT)
|
||||
FILE_STAT = FileStat(name="x.txt",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
|
||||
def make_accessor(content_search: bool = True) -> DropboxAccessor:
|
||||
|
||||
@@ -5,7 +5,8 @@ import pytest
|
||||
from mirage.commands.builtin.generic.archive import walk as aw
|
||||
from mirage.commands.builtin.generic.archive.types import Walked
|
||||
from mirage.ops.types import LinkView, MountView
|
||||
from mirage.types import LINK_TARGET_KEY, FileStat, FileType, PathSpec
|
||||
from mirage.types import (LINK_TARGET_KEY, ContentType, FileStat, FileType,
|
||||
PathSpec)
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
from mirage.utils.path import CycleError
|
||||
|
||||
@@ -29,7 +30,8 @@ class _Tree:
|
||||
return FileStat(name=key, type=FileType.DIRECTORY)
|
||||
if key in self.files:
|
||||
return FileStat(name=key,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=len(self.files[key]))
|
||||
raise FileNotFoundError(key)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import errno
|
||||
|
||||
from mirage.commands.builtin.generic.crossmount.relay.ls import run_ls
|
||||
from mirage.ops.types import MountView, NamespaceView
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
# Two mounts, /a/ and /b/, as a plain virtual-path tree. The relayed
|
||||
# primitives route by full virtual path, so one table stands for both,
|
||||
@@ -62,7 +62,8 @@ def make_dispatch(calls: Calls, roots: frozenset[str] = frozenset()):
|
||||
own = "/" if virtual in roots else virtual.rsplit("/", 1)[-1]
|
||||
return FileStat(name=own,
|
||||
size=0 if is_dir else 3,
|
||||
type=FileType.DIRECTORY if is_dir else FileType.TEXT,
|
||||
type=FileType.DIRECTORY if is_dir else FileType.FILE,
|
||||
content=None if is_dir else ContentType.TEXT,
|
||||
mode=0o755 if is_dir else 0o644), None
|
||||
|
||||
return dispatch
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
import pytest
|
||||
|
||||
from mirage.commands.builtin.generic.cp import CpFlags, cp
|
||||
from mirage.types import (FileStat, FileType, NativeCopy, PathSpec,
|
||||
PrimitiveCopy)
|
||||
from mirage.types import (ContentType, FileStat, FileType, NativeCopy,
|
||||
PathSpec, PrimitiveCopy)
|
||||
from mirage.utils.errors import enotsup
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ def _make_backend(files: dict[str, bytes],
|
||||
return FileStat(name=k.rsplit("/", 1)[-1], type=FileType.DIRECTORY)
|
||||
if k in files:
|
||||
return FileStat(name=k.rsplit("/", 1)[-1],
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
modified=stamps.get(k))
|
||||
raise FileNotFoundError(k)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from mirage.commands.builtin.generic.file import file_cmd
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _spec(path: str) -> PathSpec:
|
||||
@@ -10,14 +10,17 @@ def _spec(path: str) -> PathSpec:
|
||||
resource_path=path.strip("/"))
|
||||
|
||||
|
||||
def _make_backend(files: dict[str, tuple[bytes, FileType]], dirs: set[str]):
|
||||
def _make_backend(files: dict[str, tuple[bytes, ContentType]], dirs: set[str]):
|
||||
|
||||
async def stat_fn(p: PathSpec) -> FileStat:
|
||||
if p.virtual in dirs:
|
||||
return FileStat(name=p.virtual, type=FileType.DIRECTORY, size=0)
|
||||
if p.virtual in files:
|
||||
data, ftype = files[p.virtual]
|
||||
return FileStat(name=p.virtual, type=ftype, size=len(data))
|
||||
return FileStat(name=p.virtual,
|
||||
type=FileType.FILE,
|
||||
content=ftype,
|
||||
size=len(data))
|
||||
raise FileNotFoundError(p.virtual)
|
||||
|
||||
async def read_bytes(p: PathSpec) -> bytes:
|
||||
@@ -29,7 +32,7 @@ def _make_backend(files: dict[str, tuple[bytes, FileType]], dirs: set[str]):
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_single_text():
|
||||
stat_fn, read_bytes = _make_backend(
|
||||
{"/a.txt": (b"hello world\n", FileType.TEXT)}, set())
|
||||
{"/a.txt": (b"hello world\n", ContentType.TEXT)}, set())
|
||||
out, io = await file_cmd([_spec("/a.txt")],
|
||||
read_bytes=read_bytes,
|
||||
stat_fn=stat_fn)
|
||||
@@ -41,8 +44,8 @@ async def test_file_single_text():
|
||||
async def test_file_multiple_paths_one_line_each():
|
||||
stat_fn, read_bytes = _make_backend(
|
||||
{
|
||||
"/a.txt": (b"hello\n", FileType.TEXT),
|
||||
"/b.json": (b'{"k": 1}\n', FileType.JSON),
|
||||
"/a.txt": (b"hello\n", ContentType.TEXT),
|
||||
"/b.json": (b'{"k": 1}\n', ContentType.JSON),
|
||||
}, set())
|
||||
out, _io = await file_cmd(
|
||||
[_spec("/a.txt"), _spec("/b.json")],
|
||||
@@ -64,7 +67,7 @@ async def test_file_directory_reported_without_read():
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_brief_drops_path_prefix():
|
||||
stat_fn, read_bytes = _make_backend(
|
||||
{"/a.txt": (b"hello\n", FileType.TEXT)}, set())
|
||||
{"/a.txt": (b"hello\n", ContentType.TEXT)}, set())
|
||||
out, _io = await file_cmd([_spec("/a.txt")],
|
||||
read_bytes=read_bytes,
|
||||
stat_fn=stat_fn,
|
||||
|
||||
@@ -12,7 +12,8 @@ from mirage.commands.builtin.generic.find import (FindArgs, apply_mount_prefix,
|
||||
from mirage.commands.errors import FindParseError
|
||||
from mirage.ops.types import LinkView
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import FileStat, FileType, FindType, MountMode, PathSpec
|
||||
from mirage.types import (ContentType, FileStat, FileType, FindType, MountMode,
|
||||
PathSpec)
|
||||
from mirage.workspace import Workspace
|
||||
|
||||
|
||||
@@ -124,7 +125,11 @@ async def test_apply_mtime_filter_keeps_within_window():
|
||||
iso = now.isoformat()
|
||||
|
||||
async def stat(_spec: PathSpec) -> FileStat:
|
||||
return FileStat(name="a.txt", size=1, modified=iso, type=FileType.TEXT)
|
||||
return FileStat(name="a.txt",
|
||||
size=1,
|
||||
modified=iso,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
out = await apply_mtime_filter(
|
||||
["/a.txt"],
|
||||
@@ -138,8 +143,11 @@ async def test_apply_mtime_filter_keeps_within_window():
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_mtime_filter_stats_the_mounted_virtual_path():
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
stat = AsyncMock(return_value=FileStat(
|
||||
name="a.txt", size=1, modified=now.isoformat(), type=FileType.TEXT))
|
||||
stat = AsyncMock(return_value=FileStat(name="a.txt",
|
||||
size=1,
|
||||
modified=now.isoformat(),
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT))
|
||||
|
||||
out = await apply_mtime_filter(
|
||||
["/a.txt"],
|
||||
@@ -163,7 +171,8 @@ async def test_apply_mtime_filter_drops_outside_window():
|
||||
return FileStat(name="a.txt",
|
||||
size=1,
|
||||
modified=old.isoformat(),
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
out = await apply_mtime_filter(
|
||||
["/a.txt"],
|
||||
@@ -181,7 +190,8 @@ async def test_apply_mtime_filter_drops_entries_with_no_modified_time():
|
||||
return FileStat(name="a.txt",
|
||||
size=1,
|
||||
modified=None,
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
out = await apply_mtime_filter(
|
||||
["/a.txt"],
|
||||
@@ -205,7 +215,8 @@ async def test_apply_mtime_filter_honours_a_reported_utc_offset():
|
||||
return FileStat(name="a.txt",
|
||||
size=1,
|
||||
modified=moment.isoformat(),
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
out = await apply_mtime_filter(
|
||||
["/a.txt"],
|
||||
@@ -224,7 +235,8 @@ async def test_apply_mtime_filter_drops_a_malformed_timestamp():
|
||||
return FileStat(name="a.txt",
|
||||
size=1,
|
||||
modified="not-a-date",
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
out = await apply_mtime_filter(
|
||||
["/a.txt"],
|
||||
@@ -338,16 +350,27 @@ async def test_walk_find_empty_matches_empty_files_and_dirs():
|
||||
|
||||
async def stat(spec: PathSpec, _index):
|
||||
stats = {
|
||||
"/": FileStat(name="/", type=FileType.DIRECTORY),
|
||||
"/empty.txt": FileStat(name="empty.txt",
|
||||
size=0,
|
||||
type=FileType.TEXT),
|
||||
"/full.txt": FileStat(name="full.txt", size=1, type=FileType.TEXT),
|
||||
"/empty-dir": FileStat(name="empty-dir", type=FileType.DIRECTORY),
|
||||
"/full-dir": FileStat(name="full-dir", type=FileType.DIRECTORY),
|
||||
"/full-dir/a.txt": FileStat(name="a.txt",
|
||||
size=1,
|
||||
type=FileType.TEXT),
|
||||
"/":
|
||||
FileStat(name="/", type=FileType.DIRECTORY),
|
||||
"/empty.txt":
|
||||
FileStat(name="empty.txt",
|
||||
size=0,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT),
|
||||
"/full.txt":
|
||||
FileStat(name="full.txt",
|
||||
size=1,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT),
|
||||
"/empty-dir":
|
||||
FileStat(name="empty-dir", type=FileType.DIRECTORY),
|
||||
"/full-dir":
|
||||
FileStat(name="full-dir", type=FileType.DIRECTORY),
|
||||
"/full-dir/a.txt":
|
||||
FileStat(name="a.txt",
|
||||
size=1,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT),
|
||||
}
|
||||
return stats[spec.virtual]
|
||||
|
||||
@@ -368,7 +391,8 @@ async def test_walk_find_not_negates_predicate():
|
||||
return FileStat(name="/", type=FileType.DIRECTORY)
|
||||
return FileStat(name=spec.virtual.rsplit("/", 1)[-1],
|
||||
size=1,
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
results = await walk_find(_root_spec(),
|
||||
readdir=readdir,
|
||||
@@ -526,8 +550,11 @@ async def test_find_file_start_point_is_reported_not_walked():
|
||||
[_file_spec()],
|
||||
(),
|
||||
find_core=_unreached_core,
|
||||
stat_path=_stat_path(FileStat(name="a.txt", size=6,
|
||||
type=FileType.TEXT)),
|
||||
stat_path=_stat_path(
|
||||
FileStat(name="a.txt",
|
||||
size=6,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)),
|
||||
)
|
||||
assert io.exit_code == 0
|
||||
assert stdout == b"/mnt/a.txt\n"
|
||||
@@ -535,7 +562,10 @@ async def test_find_file_start_point_is_reported_not_walked():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_file_start_point_type_filters():
|
||||
start = FileStat(name="a.txt", size=6, type=FileType.TEXT)
|
||||
start = FileStat(name="a.txt",
|
||||
size=6,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
for ftype, expected in (("f", b"/mnt/a.txt\n"), ("d", b""), ("l", b"")):
|
||||
stdout, io = await find([_file_spec()], ("-type", ftype),
|
||||
find_core=_unreached_core,
|
||||
@@ -546,7 +576,10 @@ async def test_find_file_start_point_type_filters():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_file_start_point_depth_and_size():
|
||||
start = FileStat(name="a.txt", size=6, type=FileType.TEXT)
|
||||
start = FileStat(name="a.txt",
|
||||
size=6,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
cases = [
|
||||
({
|
||||
"maxdepth": "0"
|
||||
@@ -603,8 +636,11 @@ async def test_find_file_start_point_respells_the_operand():
|
||||
[spec],
|
||||
(),
|
||||
find_core=_unreached_core,
|
||||
stat_path=_stat_path(FileStat(name="a.txt", size=6,
|
||||
type=FileType.TEXT)),
|
||||
stat_path=_stat_path(
|
||||
FileStat(name="a.txt",
|
||||
size=6,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)),
|
||||
)
|
||||
assert stdout == b"/other/link.txt\n"
|
||||
|
||||
@@ -867,7 +903,10 @@ def _stat_map(stats: dict[str, FileStat | None]):
|
||||
|
||||
|
||||
_DIR_STAT = FileStat(name="d", type=FileType.DIRECTORY)
|
||||
_FILE_STAT = FileStat(name="f", size=6, type=FileType.TEXT)
|
||||
_FILE_STAT = FileStat(name="f",
|
||||
size=6,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
# GNU findutils 4.10.0, pinned on debian:stable-slim:
|
||||
# find A B -> A's rows, then B's rows (operand order, never
|
||||
@@ -973,7 +1012,7 @@ async def test_walk_find_reports_a_directory_it_may_not_open():
|
||||
"/": FileType.DIRECTORY,
|
||||
"/open": FileType.DIRECTORY,
|
||||
"/sealed": FileType.DIRECTORY,
|
||||
"/open/o": FileType.TEXT,
|
||||
"/open/o": FileType.FILE,
|
||||
}
|
||||
|
||||
async def readdir(spec, index=None):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from mirage.commands.builtin.generic.grep import grep
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ def _make_backend(files: dict[str, bytes], dirs: set[str] | None = None):
|
||||
if p in files:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1] or p,
|
||||
size=len(files[p]),
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
if p.rstrip("/") in inferred_dirs or p in inferred_dirs:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1] or "/",
|
||||
type=FileType.DIRECTORY)
|
||||
@@ -318,7 +319,8 @@ def _make_prefixed_backend(files: dict[str, bytes], mount_prefix: str):
|
||||
if p in full_files:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1],
|
||||
size=len(full_files[p]),
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
if p.rstrip("/") in inferred_dirs:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1] or "/",
|
||||
type=FileType.DIRECTORY)
|
||||
|
||||
@@ -9,8 +9,8 @@ from mirage.commands.builtin.generic.ls import (LS_FAILURE, LS_MINOR_PROBLEM,
|
||||
exit_status_for, format_simple,
|
||||
ls, walk)
|
||||
from mirage.ops.types import LinkView, MountView
|
||||
from mirage.types import (LINK_TARGET_KEY, FileStat, FileType, LsSortBy,
|
||||
PathSpec)
|
||||
from mirage.types import (LINK_TARGET_KEY, ContentType, FileStat, FileType,
|
||||
LsSortBy, PathSpec)
|
||||
|
||||
|
||||
def _spec(path: str) -> PathSpec:
|
||||
@@ -74,7 +74,8 @@ def _file(name: str, size: int = 0, modified: str | None = None) -> FileStat:
|
||||
return FileStat(name=name,
|
||||
size=size,
|
||||
modified=modified,
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
|
||||
def _dir(name: str) -> FileStat:
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
import pytest
|
||||
|
||||
from mirage.commands.builtin.generic.mv import MvFlags, mv
|
||||
from mirage.types import (FileStat, FileType, NativeMove, PathSpec,
|
||||
PrimitiveMove)
|
||||
from mirage.types import (ContentType, FileStat, FileType, NativeMove,
|
||||
PathSpec, PrimitiveMove)
|
||||
from mirage.utils.errors import enoent, enotdir, enotsup
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ def _make_backend(files: dict[str, bytes],
|
||||
return FileStat(name=k.rsplit("/", 1)[-1], type=FileType.DIRECTORY)
|
||||
if k in files:
|
||||
return FileStat(name=k.rsplit("/", 1)[-1],
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
modified=stamps.get(k))
|
||||
raise FileNotFoundError(k)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from mirage.commands.builtin.generic.cmp import cmp_cmd
|
||||
from mirage.commands.builtin.generic.md5 import md5
|
||||
from mirage.commands.builtin.generic.sha256sum import sha256sum_generic
|
||||
from mirage.commands.config import CommandOpts
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _spec(path: str) -> PathSpec:
|
||||
@@ -39,7 +39,10 @@ def _make_stat(files: dict[str, bytes]):
|
||||
key = path.virtual if isinstance(path, PathSpec) else path
|
||||
if key not in files:
|
||||
raise FileNotFoundError(key)
|
||||
return FileStat(name=key, size=len(files[key]), type=FileType.TEXT)
|
||||
return FileStat(name=key,
|
||||
size=len(files[key]),
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
return stat
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
from mirage.commands.builtin.generic.cut import cut
|
||||
from mirage.commands.builtin.generic.file import file_cmd
|
||||
from mirage.commands.builtin.generic.stat import stat as generic_stat
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _spec(path: str) -> PathSpec:
|
||||
@@ -118,7 +118,8 @@ async def test_stat_default_format():
|
||||
return FileStat(name=path.virtual,
|
||||
size=42,
|
||||
modified="2026-01-01",
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
out, _ = await generic_stat([_spec("a.txt")], stat_fn=stat_fn)
|
||||
assert b"name=a.txt" in out
|
||||
@@ -130,7 +131,10 @@ async def test_stat_default_format():
|
||||
async def test_stat_custom_format():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="foo", size=10, type=FileType.TEXT)
|
||||
return FileStat(name="foo",
|
||||
size=10,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
out, _ = await generic_stat([_spec("foo")], stat_fn=stat_fn, c="%n=%s")
|
||||
assert out == b"foo=10\n"
|
||||
@@ -150,7 +154,7 @@ async def test_stat_format_F_directory():
|
||||
async def test_stat_format_F_regular():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="x", type=FileType.JSON)
|
||||
return FileStat(name="x", type=FileType.FILE, content=ContentType.JSON)
|
||||
|
||||
out, _ = await generic_stat([_spec("x")], stat_fn=stat_fn, f="%F")
|
||||
assert out == b"regular file\n"
|
||||
@@ -160,7 +164,10 @@ async def test_stat_format_F_regular():
|
||||
async def test_stat_multiple_paths():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name=path.virtual, size=1, type=FileType.TEXT)
|
||||
return FileStat(name=path.virtual,
|
||||
size=1,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
out, _ = await generic_stat([_spec("a"), _spec("b")],
|
||||
stat_fn=stat_fn,
|
||||
@@ -172,7 +179,7 @@ async def test_stat_multiple_paths():
|
||||
async def test_stat_missing_operand():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="x")
|
||||
return FileStat(type=FileType.FILE, name="x")
|
||||
|
||||
with pytest.raises(ValueError, match="missing operand"):
|
||||
await generic_stat([], stat_fn=stat_fn)
|
||||
@@ -182,7 +189,10 @@ async def test_stat_missing_operand():
|
||||
async def test_file_text_default():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name=path.virtual, size=5, type=FileType.TEXT)
|
||||
return FileStat(name=path.virtual,
|
||||
size=5,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
async def read_bytes(path):
|
||||
return b"hello"
|
||||
@@ -198,7 +208,10 @@ async def test_file_text_default():
|
||||
async def test_file_brief_mode():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="f", size=4, type=FileType.TEXT)
|
||||
return FileStat(name="f",
|
||||
size=4,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
async def read_bytes(path):
|
||||
return b"abcd"
|
||||
@@ -214,7 +227,10 @@ async def test_file_brief_mode():
|
||||
async def test_file_mime_mode():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="f.json", size=10, type=FileType.JSON)
|
||||
return FileStat(name="f.json",
|
||||
size=10,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON)
|
||||
|
||||
async def read_bytes(path):
|
||||
return b'{"a": 1}'
|
||||
@@ -245,7 +261,10 @@ async def test_file_directory():
|
||||
async def test_file_multiple_paths():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name=path.virtual, size=3, type=FileType.TEXT)
|
||||
return FileStat(name=path.virtual,
|
||||
size=3,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
async def read_bytes(path):
|
||||
return b"abc"
|
||||
@@ -262,7 +281,10 @@ async def test_file_multiple_paths():
|
||||
async def test_file_read_error_logs_and_falls_back():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="x", size=1, type=FileType.TEXT)
|
||||
return FileStat(name="x",
|
||||
size=1,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
async def read_bytes(path):
|
||||
raise OSError("denied")
|
||||
@@ -277,7 +299,7 @@ async def test_file_read_error_logs_and_falls_back():
|
||||
async def test_file_missing_operand():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="x")
|
||||
return FileStat(type=FileType.FILE, name="x")
|
||||
|
||||
async def read_bytes(path):
|
||||
return b""
|
||||
|
||||
@@ -5,7 +5,7 @@ from mirage.commands.builtin.generic.dirname import dirname
|
||||
from mirage.commands.builtin.generic.mktemp import mktemp
|
||||
from mirage.commands.builtin.generic.readlink import readlink
|
||||
from mirage.commands.builtin.generic.realpath import realpath
|
||||
from mirage.types import FileStat, PathSpec
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ async def test_dirname_multiple():
|
||||
async def test_realpath_normalizes():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="x")
|
||||
return FileStat(type=FileType.FILE, name="x")
|
||||
|
||||
out, _ = await realpath([_spec("/a/./b/../c")], stat_fn=stat_fn)
|
||||
assert out == b"/a/c\n"
|
||||
@@ -66,7 +66,7 @@ async def test_realpath_normalizes():
|
||||
async def test_realpath_exists_check_passes():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="x")
|
||||
return FileStat(type=FileType.FILE, name="x")
|
||||
|
||||
out, _ = await realpath([_spec("/a/b")], stat_fn=stat_fn, e=True)
|
||||
assert out == b"/a/b\n"
|
||||
@@ -89,7 +89,7 @@ async def test_realpath_exists_check_fails():
|
||||
async def test_realpath_multiple():
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name="x")
|
||||
return FileStat(type=FileType.FILE, name="x")
|
||||
|
||||
out, _ = await realpath([_spec("/a"), _spec("/b/../c")], stat_fn=stat_fn)
|
||||
assert out == b"/a\n/c\n"
|
||||
|
||||
@@ -5,7 +5,7 @@ from mirage.commands.builtin.generic.jq import jq
|
||||
from mirage.commands.builtin.generic.patch import patch
|
||||
from mirage.commands.builtin.generic.tsort import tsort
|
||||
from mirage.commands.builtin.generic.unzip import unzip
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ def _make_backend(files: dict[str, bytes]):
|
||||
|
||||
|
||||
async def _stat_file(path) -> FileStat:
|
||||
return FileStat(name=path.virtual, type=FileType.TEXT)
|
||||
return FileStat(name=path.virtual,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from mirage.commands.builtin.generic.rg import parse_flags, rg
|
||||
from mirage.commands.spec.types import FlagView
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ def _make_backend(files: dict[str, bytes], dirs: set[str] | None = None):
|
||||
if p in files:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1] or p,
|
||||
size=len(files[p]),
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
if p.rstrip("/") in inferred_dirs or p in inferred_dirs:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1] or "/",
|
||||
type=FileType.DIRECTORY)
|
||||
@@ -361,7 +362,8 @@ def _make_prefixed_backend(files: dict[str, bytes], mount_prefix: str):
|
||||
if p in full_files:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1],
|
||||
size=len(full_files[p]),
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
if p.rstrip("/") in inferred_dirs:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1] or "/",
|
||||
type=FileType.DIRECTORY)
|
||||
|
||||
@@ -6,8 +6,8 @@ from mirage.commands.builtin.generic.stat import stat
|
||||
from mirage.io.types import materialize
|
||||
from mirage.ops.types import LinkView
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import (LINK_TARGET_KEY, FileStat, FileType, MountMode,
|
||||
PathSpec)
|
||||
from mirage.types import (LINK_TARGET_KEY, ContentType, FileStat, FileType,
|
||||
MountMode, PathSpec)
|
||||
from mirage.workspace import Workspace
|
||||
|
||||
_MTIME = "2026-01-02T15:30:45Z"
|
||||
@@ -27,8 +27,11 @@ def _fs(**kw: object) -> FileStat:
|
||||
base: dict[str, object] = dict(name="f.txt",
|
||||
size=6,
|
||||
modified=_MTIME,
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
base.update(kw)
|
||||
if base.get("type") is not FileType.FILE and "content" not in kw:
|
||||
base.pop("content", None)
|
||||
return FileStat(**base)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ from mirage.commands.builtin.generic.archive.types import Walked
|
||||
from mirage.commands.builtin.generic.tar import (excluded, member_name, pruned,
|
||||
strip_prefix, tar)
|
||||
from mirage.ops.types import LinkView, MountView
|
||||
from mirage.types import LINK_TARGET_KEY, FileStat, FileType, PathSpec
|
||||
from mirage.types import (LINK_TARGET_KEY, ContentType, FileStat, FileType,
|
||||
PathSpec)
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
from mirage.utils.path import CycleError
|
||||
|
||||
@@ -60,7 +61,8 @@ class _Tree:
|
||||
return FileStat(name=key, type=FileType.DIRECTORY)
|
||||
if key in self.files:
|
||||
return FileStat(name=key,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=len(self.files[key]))
|
||||
raise FileNotFoundError(key)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
|
||||
from mirage.commands.builtin.generic.tree import tree
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _spec(path: str) -> PathSpec:
|
||||
@@ -13,7 +13,10 @@ def _spec(path: str) -> PathSpec:
|
||||
|
||||
|
||||
def _file(name: str, size: int = 0) -> FileStat:
|
||||
return FileStat(name=name, size=size, type=FileType.TEXT)
|
||||
return FileStat(name=name,
|
||||
size=size,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
|
||||
def _dir(name: str) -> FileStat:
|
||||
@@ -467,15 +470,24 @@ async def test_tree_marks_a_subdirectory_it_may_not_open_and_exits_2():
|
||||
"/r":
|
||||
FileStat(name="r", type=FileType.DIRECTORY),
|
||||
"/r/a":
|
||||
FileStat(name="a", type=FileType.TEXT, size=1),
|
||||
FileStat(name="a",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=1),
|
||||
"/r/locked":
|
||||
FileStat(name="locked", type=FileType.DIRECTORY),
|
||||
"/r/locked/y":
|
||||
FileStat(name="y", type=FileType.TEXT, size=1),
|
||||
FileStat(name="y",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=1),
|
||||
"/r/sub":
|
||||
FileStat(name="sub", type=FileType.DIRECTORY),
|
||||
"/r/sub/y":
|
||||
FileStat(name="y", type=FileType.TEXT, size=1),
|
||||
FileStat(name="y",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=1),
|
||||
})
|
||||
|
||||
async def guarded(p: PathSpec, index=None) -> list[str]:
|
||||
|
||||
@@ -7,7 +7,8 @@ from mirage.commands.builtin.generic.archive.types import Walked
|
||||
from mirage.commands.builtin.generic.zip_cmd import (excluded, member_name,
|
||||
zip_cmd)
|
||||
from mirage.ops.types import LinkView, MountView
|
||||
from mirage.types import LINK_TARGET_KEY, FileStat, FileType, PathSpec
|
||||
from mirage.types import (LINK_TARGET_KEY, ContentType, FileStat, FileType,
|
||||
PathSpec)
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@@ -53,7 +54,8 @@ class _Tree:
|
||||
return FileStat(name=key, type=FileType.DIRECTORY)
|
||||
if key in self.files:
|
||||
return FileStat(name=key,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=len(self.files[key]))
|
||||
raise FileNotFoundError(key)
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@ def _ops(max_du_entries: int | None = None) -> CommandIO:
|
||||
if virtual in TREE:
|
||||
return FileStat(name=virtual, type=FileType.DIRECTORY)
|
||||
if virtual in SIZES:
|
||||
return FileStat(name=virtual, size=SIZES[virtual])
|
||||
return FileStat(type=FileType.FILE,
|
||||
name=virtual,
|
||||
size=SIZES[virtual])
|
||||
raise FileNotFoundError(virtual)
|
||||
|
||||
async def read_bytes(_accessor, _path, _index=None):
|
||||
|
||||
@@ -41,7 +41,8 @@ def _ops(stat_calls: list[str], find_op=None) -> CommandIO:
|
||||
return FileStat(name=path.virtual,
|
||||
type=FileType.DIRECTORY,
|
||||
modified="2099-01-01T00:00:00+00:00")
|
||||
return FileStat(name=path.virtual,
|
||||
return FileStat(type=FileType.FILE,
|
||||
name=path.virtual,
|
||||
size=3,
|
||||
modified="2099-01-01T00:00:00+00:00")
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from mirage.accessor.base import NOOPAccessor
|
||||
from mirage.commands.builtin.generic_bind.adapter import (CommandIO, Operation,
|
||||
dir_aware_stat,
|
||||
dir_aware_stream)
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.glob_walk import DEFAULT_MAX_GLOB_MATCHES
|
||||
|
||||
TREE = {
|
||||
@@ -94,7 +94,7 @@ def _probe_ops(missing: set[str],
|
||||
raise FileNotFoundError(path.virtual)
|
||||
if path.virtual in typed:
|
||||
return FileStat(name=path.virtual, type=FileType.DIRECTORY)
|
||||
return FileStat(name=path.virtual, size=0)
|
||||
return FileStat(type=FileType.FILE, name=path.virtual, size=0)
|
||||
|
||||
async def readdir(_accessor, path, _index):
|
||||
target = path.virtual.rstrip("/") or "/"
|
||||
@@ -241,7 +241,10 @@ async def test_rule_guard_asks_the_bound_gate_and_leaves_stat_alone():
|
||||
|
||||
async def stat(accessor, path, index=None):
|
||||
calls.append(("stat", path.virtual))
|
||||
return FileStat(name="k", type=FileType.TEXT, size=1)
|
||||
return FileStat(name="k",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=1)
|
||||
|
||||
async def readdir(accessor, path, index=None):
|
||||
calls.append(("readdir", path.virtual))
|
||||
|
||||
@@ -26,7 +26,7 @@ from mirage.commands.config import CommandOpts
|
||||
from mirage.commands.registry import command
|
||||
from mirage.commands.spec import SPECS
|
||||
from mirage.provision import Precision
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
SIZES = {
|
||||
@@ -57,7 +57,8 @@ async def _stat(accessor, path, index=None) -> FileStat:
|
||||
type=FileType.DIRECTORY)
|
||||
return FileStat(name=virtual.rsplit("/", 1)[-1],
|
||||
size=SIZES.get(virtual),
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
|
||||
async def _readdir(accessor, path, index=None) -> list[str]:
|
||||
@@ -309,7 +310,10 @@ async def test_index_hit_read_provision_without_paths_is_unknown():
|
||||
|
||||
|
||||
async def _noop_stat(*args, **kwargs):
|
||||
return FileStat(name="x", size=0, type=FileType.FILE)
|
||||
return FileStat(name="x",
|
||||
size=0,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.FILE)
|
||||
|
||||
|
||||
def _make_command(name: str, provision=None, filetype: str | None = None):
|
||||
|
||||
@@ -25,7 +25,7 @@ from mirage.commands.builtin.generic_bind.builders.shuf import \
|
||||
from mirage.commands.builtin.utils.wrap import stream_from_bytes
|
||||
from mirage.commands.config import CommandOpts
|
||||
from mirage.io.types import materialize
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
_FILES = {
|
||||
"/g/a.txt": b"alpha\n",
|
||||
@@ -52,7 +52,8 @@ async def _stat(accessor, path, index=None):
|
||||
if p in _FILES:
|
||||
return FileStat(name=p.rsplit("/", 1)[-1],
|
||||
size=len(_FILES[p]),
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
raise FileNotFoundError(p)
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from mirage.commands.config import CommandOpts
|
||||
from mirage.core.hierarchy.scope import ScopeMatch
|
||||
from mirage.core.hierarchy.search import SearchQuery
|
||||
from mirage.io.types import ByteSource
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.errors import enoent
|
||||
from tests.core.hierarchy.conftest import FakeAccessor, detect_scope, spec
|
||||
|
||||
@@ -42,7 +42,10 @@ async def _read_op(accessor: FakeAccessor,
|
||||
async def _stat_op(accessor: FakeAccessor,
|
||||
path: PathSpec,
|
||||
index=NULL_INDEX) -> FileStat:
|
||||
return FileStat(name="a.json", type=FileType.JSON, size=len(CONTENT))
|
||||
return FileStat(name="a.json",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.JSON,
|
||||
size=len(CONTENT))
|
||||
|
||||
|
||||
async def _readdir_op(accessor: FakeAccessor,
|
||||
|
||||
@@ -24,7 +24,7 @@ from mirage.commands.builtin.generic_bind import CommandIO
|
||||
from mirage.commands.builtin.mongodb.cat import cat
|
||||
from mirage.commands.config import CommandOpts
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
from mirage.types import FileStat, PathSpec
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -38,7 +38,7 @@ def _path(s: str = "/db1/collections/coll1/documents.jsonl") -> PathSpec:
|
||||
|
||||
|
||||
async def _fake_stat(_accessor, path, index=None):
|
||||
return FileStat(name=path.virtual, size=None)
|
||||
return FileStat(type=FileType.FILE, name=path.virtual, size=None)
|
||||
|
||||
|
||||
async def _unused(*_args, **_kwargs):
|
||||
|
||||
@@ -23,7 +23,7 @@ from mirage.commands.builtin.object_store.stat import make_stat
|
||||
from mirage.commands.config import CommandOpts
|
||||
from mirage.io.types import materialize
|
||||
from mirage.ops.types import NamespaceView, StatOverlay
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
_BACKEND_MTIME = "2020-05-05T05:05:05Z"
|
||||
_OVERLAY_MTIME = "2024-01-01T00:00:00Z"
|
||||
@@ -34,7 +34,8 @@ def _backend_stat() -> FileStat:
|
||||
size=6,
|
||||
modified=_BACKEND_MTIME,
|
||||
mode=0o644,
|
||||
type=FileType.TEXT)
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
|
||||
async def _fake_stat_core(_accessor: Accessor,
|
||||
|
||||
@@ -21,7 +21,7 @@ from mirage.commands.builtin.slack.grep import grep
|
||||
from mirage.commands.builtin.slack.rg import rg
|
||||
from mirage.commands.config import CommandOpts
|
||||
from mirage.io.types import IOResult
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@@ -187,8 +187,10 @@ async def test_grep_falls_back_when_native_search_raises():
|
||||
new=AsyncMock(return_value=b""),
|
||||
), patch(
|
||||
"mirage.commands.builtin.slack.grep._stat",
|
||||
new=AsyncMock(return_value=FileStat(
|
||||
name="chat.jsonl", type=FileType.TEXT, size=0)),
|
||||
new=AsyncMock(return_value=FileStat(name="chat.jsonl",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=0)),
|
||||
):
|
||||
out, io = await grep(
|
||||
accessor, paths, ['hello'],
|
||||
|
||||
@@ -22,7 +22,7 @@ from mirage.commands.builtin.slack.rg import rg
|
||||
from mirage.commands.config import CommandOpts
|
||||
from mirage.core.slack.config import SlackConfig
|
||||
from mirage.io.stream import materialize
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@@ -157,7 +157,8 @@ async def test_grep_files_dir_redirects_to_per_file_scan(accessor, index):
|
||||
patch("mirage.commands.builtin.slack.grep._stat",
|
||||
new_callable=AsyncMock,
|
||||
return_value=FileStat(name="report.txt",
|
||||
type=FileType.TEXT)),
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)),
|
||||
):
|
||||
from mirage.commands.builtin.slack.grep import grep
|
||||
out, io = await grep(accessor, [
|
||||
|
||||
@@ -23,7 +23,7 @@ from mirage.commands.builtin.generic.rg import rg as generic_rg
|
||||
from mirage.commands.builtin.generic.tail import tail_multi
|
||||
from mirage.commands.builtin.generic.wc import format_multi
|
||||
from mirage.io.types import materialize
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
_PAYLOAD = b"alpha\nbeta\n"
|
||||
@@ -53,7 +53,10 @@ async def _warm_manager() -> CacheManager:
|
||||
|
||||
|
||||
async def _stat(path) -> FileStat:
|
||||
return FileStat(name="a.txt", type=FileType.TEXT, size=len(_PAYLOAD))
|
||||
return FileStat(name="a.txt",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=len(_PAYLOAD))
|
||||
|
||||
|
||||
async def _readdir(path) -> list[str]:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from mirage.commands.builtin.find_helper import (expand_printf,
|
||||
printf_needs_stat,
|
||||
unrespell_raw)
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _spec(virtual: str, raw: str | None = None) -> PathSpec:
|
||||
@@ -12,10 +12,17 @@ def _spec(virtual: str, raw: str | None = None) -> PathSpec:
|
||||
raw_path=raw if raw is not None else virtual)
|
||||
|
||||
|
||||
def _stat(size: int = 6, file_type: FileType = FileType.TEXT) -> FileStat:
|
||||
def _stat(size: int = 6,
|
||||
file_type: ContentType | FileType = ContentType.TEXT) -> FileStat:
|
||||
if isinstance(file_type, FileType):
|
||||
return FileStat(name="a",
|
||||
size=size,
|
||||
type=file_type,
|
||||
modified="2026-08-16T13:45:30+00:00")
|
||||
return FileStat(name="a",
|
||||
size=size,
|
||||
type=file_type,
|
||||
type=FileType.FILE,
|
||||
content=file_type,
|
||||
modified="2026-08-16T13:45:30+00:00")
|
||||
|
||||
|
||||
@@ -56,7 +63,11 @@ def test_expand_stat_directives():
|
||||
def test_expand_reported_mode_over_default():
|
||||
warnings: list[str] = []
|
||||
search = _spec("/data")
|
||||
st = FileStat(name="a", size=6, type=FileType.TEXT, mode=0o600)
|
||||
st = FileStat(name="a",
|
||||
size=6,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
mode=0o600)
|
||||
assert expand_printf("%m %M\n", "/data/a.txt", search, st,
|
||||
warnings) == "600 -rw-------\n"
|
||||
d = FileStat(name="sub", size=0, type=FileType.DIRECTORY, mode=0o700)
|
||||
|
||||
@@ -16,7 +16,7 @@ import pytest
|
||||
|
||||
from mirage.commands.builtin.utils.formatting import (_human_size,
|
||||
format_ls_long)
|
||||
from mirage.types import FileStat, FileType
|
||||
from mirage.types import ContentType, FileStat, FileType
|
||||
|
||||
# Read off GNU coreutils 9.7 (`ls -lh` on a file of each size, debian
|
||||
# stable-slim). The three rows that matter are the ones a plain
|
||||
@@ -60,7 +60,8 @@ def test_human_size_matches_gnu(size: int, expected: str):
|
||||
def test_format_ls_long_regular_file():
|
||||
stat = FileStat(name="file.txt",
|
||||
size=5,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
modified="2026-01-01T00:00:00Z")
|
||||
[line] = format_ls_long([stat])
|
||||
assert line == "-rw-r--r-- 1 user user 5 Jan 1 00:00 file.txt"
|
||||
@@ -80,11 +81,13 @@ def test_format_ls_long_size_alignment():
|
||||
stats = [
|
||||
FileStat(name="a",
|
||||
size=5,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
modified="2026-01-01T00:00:00Z"),
|
||||
FileStat(name="b",
|
||||
size=1234,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
modified="2026-01-01T00:00:00Z"),
|
||||
]
|
||||
lines = format_ls_long(stats)
|
||||
@@ -95,7 +98,8 @@ def test_format_ls_long_size_alignment():
|
||||
def test_format_ls_long_human_size():
|
||||
stat = FileStat(name="big",
|
||||
size=2048,
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
modified="2026-01-01T00:00:00Z")
|
||||
[line] = format_ls_long([stat], human=True)
|
||||
assert "2.0K" in line
|
||||
@@ -103,6 +107,10 @@ def test_format_ls_long_human_size():
|
||||
|
||||
|
||||
def test_format_ls_long_missing_modified():
|
||||
stat = FileStat(name="x", size=0, type=FileType.TEXT, modified=None)
|
||||
stat = FileStat(name="x",
|
||||
size=0,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
modified=None)
|
||||
[line] = format_ls_long([stat])
|
||||
assert "Jan 1 00:00" in line
|
||||
|
||||
@@ -28,7 +28,7 @@ from mirage.core.ram.read import read
|
||||
from mirage.core.ram.readdir import readdir
|
||||
from mirage.core.ram.stat import stat
|
||||
from mirage.core.ram.write import write_bytes as _async_write_bytes
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
from mirage.commands.builtin.grep_helper import ( # isort: skip
|
||||
NEVER_MATCH, classify_pattern, compile_pattern, extract_required_literal,
|
||||
@@ -163,7 +163,9 @@ async def test_grep_files_only_recursive_scans_file_operands():
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
async def stat_fn(path):
|
||||
return FileStat(name=path, type=FileType.TEXT)
|
||||
return FileStat(name=path,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
|
||||
async def read_bytes_fn(path):
|
||||
return b"alpha beta\n"
|
||||
|
||||
@@ -31,7 +31,9 @@ def _stat_over(files: dict[str, bytes], dirs: set[str] | None = None):
|
||||
return FileStat(name=path.virtual, type=FileType.DIRECTORY)
|
||||
if path.virtual not in files:
|
||||
raise FileNotFoundError(path.virtual)
|
||||
return FileStat(name=path.virtual, size=len(files[path.virtual]))
|
||||
return FileStat(type=FileType.FILE,
|
||||
name=path.virtual,
|
||||
size=len(files[path.virtual]))
|
||||
|
||||
return stat
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from mirage.commands.builtin.utils.slash_links import (is_slashed_link,
|
||||
mkdir_link_refusal,
|
||||
rm_link_refusal)
|
||||
from mirage.ops.types import LinkView
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, PathSpec
|
||||
|
||||
|
||||
def _spec(virtual: str, raw_path: str) -> PathSpec:
|
||||
@@ -56,7 +56,7 @@ def _links(link_at: str | None, target: FileStat | None) -> LinkView:
|
||||
|
||||
|
||||
DIR = FileStat(name="sub", type=FileType.DIRECTORY)
|
||||
FILE = FileStat(name="reg", type=FileType.TEXT)
|
||||
FILE = FileStat(name="reg", type=FileType.FILE, content=ContentType.TEXT)
|
||||
|
||||
|
||||
def test_is_slashed_link_needs_both_the_slash_and_the_link():
|
||||
|
||||
@@ -22,7 +22,7 @@ from mirage.commands.cli.builtin.git.add import (EXECUTABLE, REGULAR, SYMLINK,
|
||||
entry_mode, keep_addable,
|
||||
staged_entry)
|
||||
from mirage.commands.cli.builtin.git.ignore import IgnoreStack
|
||||
from mirage.types import LINK_TARGET_KEY, FileStat, FileType
|
||||
from mirage.types import LINK_TARGET_KEY, ContentType, FileStat, FileType
|
||||
|
||||
|
||||
def stat(mode: int | None) -> FileStat:
|
||||
@@ -31,7 +31,12 @@ def stat(mode: int | None) -> FileStat:
|
||||
Args:
|
||||
mode (int | None): permission bits, None when it has none.
|
||||
"""
|
||||
return FileStat(name="x", path="x", type=FileType.TEXT, size=4, mode=mode)
|
||||
return FileStat(name="x",
|
||||
path="x",
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=4,
|
||||
mode=mode)
|
||||
|
||||
|
||||
async def run(git_rw, line: str) -> tuple[int, bytes, bytes]:
|
||||
|
||||
@@ -26,7 +26,7 @@ from mirage.commands.cli.builtin.git.changes import (conflict_codes,
|
||||
from mirage.commands.cli.builtin.git.index import read_index
|
||||
from mirage.commands.cli.builtin.git.repo import open_repo
|
||||
from mirage.commands.cli.builtin.git.types import RepoLocation, WorkTree
|
||||
from mirage.types import FileStat, FileType
|
||||
from mirage.types import ContentType, FileStat, FileType
|
||||
|
||||
REGULAR = 0o100644
|
||||
EXECUTABLE = 0o100755
|
||||
@@ -65,7 +65,8 @@ def stat(size: int | None = 4, mode: int | None = 0o644) -> FileStat:
|
||||
"""
|
||||
return FileStat(name="x",
|
||||
path="x",
|
||||
type=FileType.TEXT,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT,
|
||||
size=size,
|
||||
mode=mode)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import pytest
|
||||
from mirage.commands.cli.builtin.git.discover import discover
|
||||
from mirage.commands.cli.builtin.git.errors import ( # yapf: disable
|
||||
InvalidGitFileError, NotARepositoryError, NoWorkingDirectoryError)
|
||||
from mirage.types import FileStat, FileType
|
||||
from mirage.types import ContentType, FileStat, FileType
|
||||
|
||||
|
||||
def _stat_over(present: set[str], files: set[str] = frozenset()):
|
||||
@@ -30,7 +30,9 @@ def _stat_over(present: set[str], files: set[str] = frozenset()):
|
||||
|
||||
async def stat_path(path: str) -> FileStat | None:
|
||||
if path in files:
|
||||
return FileStat(name=path.rsplit("/", 1)[-1], type=FileType.TEXT)
|
||||
return FileStat(name=path.rsplit("/", 1)[-1],
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT)
|
||||
if path not in present:
|
||||
return None
|
||||
return FileStat(name=path.rsplit("/", 1)[-1], type=FileType.DIRECTORY)
|
||||
|
||||
@@ -19,7 +19,7 @@ import pytest
|
||||
from mirage.commands.builtin.grep_helper import compile_pattern, grep_recursive
|
||||
from mirage.commands.builtin.rg_helper import rg_full
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import FileStat, FileType, MountMode, PathSpec
|
||||
from mirage.types import ContentType, FileStat, FileType, MountMode, PathSpec
|
||||
from mirage.workspace import Workspace
|
||||
|
||||
|
||||
@@ -54,9 +54,17 @@ async def test_grep_helper_collects_warnings_on_unreadable_file():
|
||||
readdir = _make_readdir({"/": ["/good.txt", "/bad.txt"]})
|
||||
stat_fn = _make_stat({
|
||||
"/good.txt":
|
||||
FileStat(name="good.txt", size=12, modified=None, type=FileType.TEXT),
|
||||
FileStat(name="good.txt",
|
||||
size=12,
|
||||
modified=None,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT),
|
||||
"/bad.txt":
|
||||
FileStat(name="bad.txt", size=10, modified=None, type=FileType.TEXT),
|
||||
FileStat(name="bad.txt",
|
||||
size=10,
|
||||
modified=None,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT),
|
||||
})
|
||||
|
||||
async def async_readdir(path):
|
||||
@@ -132,9 +140,17 @@ async def test_rg_helper_collects_warnings_on_unreadable_file():
|
||||
readdir = _make_readdir({"/": ["/good.py", "/bad.py"]})
|
||||
stat_fn = _make_stat({
|
||||
"/good.py":
|
||||
FileStat(name="good.py", size=12, modified=None, type=FileType.TEXT),
|
||||
FileStat(name="good.py",
|
||||
size=12,
|
||||
modified=None,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT),
|
||||
"/bad.py":
|
||||
FileStat(name="bad.py", size=10, modified=None, type=FileType.TEXT),
|
||||
FileStat(name="bad.py",
|
||||
size=10,
|
||||
modified=None,
|
||||
type=FileType.FILE,
|
||||
content=ContentType.TEXT),
|
||||
})
|
||||
|
||||
async def async_readdir(path):
|
||||
|
||||
@@ -20,7 +20,7 @@ from mirage.cache.index.config import IndexEntry
|
||||
from mirage.core.box.read import read
|
||||
from mirage.core.box.readdir import readdir
|
||||
from mirage.core.box.stat import stat
|
||||
from mirage.types import FileType, PathSpec
|
||||
from mirage.types import ContentType, FileType, PathSpec
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -56,7 +56,7 @@ async def test_stat_file_carries_box_metadata(accessor, index):
|
||||
accessor,
|
||||
PathSpec(resource_path="a.txt", virtual="/a.txt", directory="/"),
|
||||
index)
|
||||
assert info.type == FileType.TEXT
|
||||
assert info.content == ContentType.TEXT
|
||||
assert info.size == 5
|
||||
assert info.modified == "2026-04-01T00:00:00+00:00"
|
||||
assert info.fingerprint == "2026-04-01T00:00:00+00:00"
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
import pytest
|
||||
|
||||
from mirage.core.chroma.stat import stat, stat_name
|
||||
from mirage.types import FileType, PathSpec
|
||||
from mirage.types import ContentType, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_file(chroma_accessor, chroma_index, quickstart_path):
|
||||
result = await stat(chroma_accessor, quickstart_path, chroma_index)
|
||||
assert result.type == FileType.TEXT
|
||||
assert result.content == ContentType.TEXT
|
||||
assert result.name == "quickstart"
|
||||
assert result.size == 12
|
||||
assert result.modified == "2026-02-01T00:00:00Z"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user