Files
strukto-ai--mirage/python/tests/core/nextcloud/conftest.py
T
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

195 lines
5.6 KiB
Python

from dataclasses import dataclass, field
from datetime import datetime, timezone
import pytest
from opendal.exceptions import NotFound
from opendal.types import EntryMode
from mirage.accessor.nextcloud import NextcloudAccessor
from mirage.resource.nextcloud import NextcloudConfig
@dataclass
class _FakeMetadata:
content_length: int = 0
mode: EntryMode = EntryMode.File
etag: str | None = None
last_modified: datetime | None = None
content_type: str | None = None
@dataclass
class _FakeEntry:
path: str
metadata: _FakeMetadata
@property
def name(self) -> str:
return self.path.rstrip("/").rsplit("/", 1)[-1]
@dataclass
class _FakeFile:
data: bytes
pos: int = 0
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return None
async def seek(self, offset: int) -> None:
self.pos = offset
async def read(self, size: int | None = None) -> bytes:
if size is None:
out = self.data[self.pos:]
self.pos = len(self.data)
else:
out = self.data[self.pos:self.pos + size]
self.pos += len(out)
return out
async def tell(self) -> int:
return self.pos
@dataclass
class FakeAsyncOperator:
files: dict[str, bytes] = field(default_factory=dict)
metas: dict[str, _FakeMetadata] = field(default_factory=dict)
dirs: set[str] = field(default_factory=set)
def __post_init__(self):
for k, data in self.files.items():
self.metas.setdefault(
k,
_FakeMetadata(
content_length=len(data),
mode=EntryMode.File,
etag=f"etag-{k}",
last_modified=datetime(2026, 1, 1, tzinfo=timezone.utc),
),
)
async def read(self, key: str) -> bytes:
if key not in self.files:
raise NotFound("path not found", key)
return self.files[key]
async def open(self, key: str, mode: str = "rb"):
if key not in self.files:
raise NotFound("path not found", key)
return _FakeFile(data=self.files[key])
async def stat(self, key: str) -> _FakeMetadata:
k = key.rstrip("/")
if key.endswith("/"):
if k + "/" in self.dirs or any(
f.startswith(k + "/") for f in self.files):
return _FakeMetadata(content_length=0, mode=EntryMode.Dir)
raise NotFound("path not found", key)
if k in self.files:
return self.metas[k]
raise NotFound("path not found", key)
async def write(self, key: str, data: bytes) -> None:
self.files[key] = bytes(data)
self.metas[key] = _FakeMetadata(
content_length=len(data),
mode=EntryMode.File,
etag=f"etag-{key}",
)
async def delete(self, key: str) -> None:
if key.endswith("/"):
self.dirs.discard(key)
return
self.files.pop(key, None)
self.metas.pop(key, None)
async def create_dir(self, key: str) -> None:
self.dirs.add(key.rstrip("/") + "/")
async def copy(self, src: str, dst: str) -> None:
if src not in self.files:
raise NotFound("path not found", src)
await self.write(dst, self.files[src])
async def rename(self, src: str, dst: str) -> None:
if src not in self.files:
raise NotFound("path not found", src)
await self.write(dst, self.files[src])
await self.delete(src)
async def remove_all(self, prefix: str) -> None:
pfx = prefix.rstrip("/")
for f in [
f for f in self.files if f == pfx or f.startswith(pfx + "/")
]:
self.files.pop(f, None)
self.metas.pop(f, None)
async def list(self, path: str):
pfx = path.lstrip("/")
seen_dirs: set[str] = set()
entries: list[_FakeEntry] = []
key = pfx.rstrip("/")
if key in self.files:
# WebDAV PROPFIND on a file returns the file itself.
entries.append(_FakeEntry(path=key, metadata=self.metas[key]))
for f in self.files:
if not f.startswith(pfx):
continue
rest = f[len(pfx):]
if "/" in rest:
subdir = rest.split("/", 1)[0]
dkey = pfx + subdir + "/"
if dkey not in seen_dirs:
seen_dirs.add(dkey)
entries.append(
_FakeEntry(
path=dkey,
metadata=_FakeMetadata(mode=EntryMode.Dir),
))
else:
entries.append(_FakeEntry(path=f, metadata=self.metas[f]))
async def _iter():
for e in entries:
yield e
return _iter()
async def scan(self, path: str):
pfx = path.lstrip("/")
entries = [
_FakeEntry(path=f, metadata=self.metas[f]) for f in self.files
if f.startswith(pfx)
]
async def _iter():
for e in entries:
yield e
return _iter()
def make_accessor(files: dict[str, bytes] | None = None) -> NextcloudAccessor:
cfg = NextcloudConfig(
url="https://cloud.example.com/remote.php/dav/files/user/",
username="user",
password="pass",
)
acc = NextcloudAccessor(cfg)
fake = FakeAsyncOperator(files=dict(files or {}))
acc._fake = fake
acc.operator = lambda: fake
return acc
@pytest.fixture
def make_acc():
return make_accessor