Skip to content

feat(sdk): filesystem metadata operations (JS + Python) - #224

Open
alitariksahin wants to merge 3 commits into
mainfrom
DX-2944
Open

feat(sdk): filesystem metadata operations (JS + Python)#224
alitariksahin wants to merge 3 commits into
mainfrom
DX-2944

Conversation

@alitariksahin

Copy link
Copy Markdown
Collaborator

Client support for the filesystem metadata endpoints added in box-backend
#237 (DX-2944), in both SDKs so the
published surface stays at parity.

New methods

TypeScript — @upstash/box

box.files.stat(path: string, options?: { follow?: boolean }): Promise<FileStat>
box.files.mkdir(path: string, options?: { parents?: boolean }): Promise<void>
box.files.rename(from: string, to: string): Promise<void>
box.files.remove(path: string, options?: { recursive?: boolean }): Promise<void>

// existing read(), now with an optional byte range
box.files.read(
  path: string,
  options?: { encoding?: "base64"; offset?: number; length?: number },
): Promise<string>

interface FileStat {
  type: "file" | "directory" | "symlink" | "other";
  size: number;
  mod_time: string;
  inode: number;
  version: string; // opaque freshness token — compare, don't parse
}

Python — upstash-box (async shown; the sync client mirrors it)

await box.files.stat(path: str, *, follow: bool = False) -> FileStat
await box.files.mkdir(path: str, *, parents: bool = False) -> None
await box.files.rename(from_path: str, to_path: str) -> None
await box.files.remove(path: str, *, recursive: bool = False) -> None

await box.files.read(
    path: str, *, encoding: str | None = None,
    offset: int | None = None, length: int | None = None,
) -> str

class FileStat(_Model):
    type: Literal["file", "directory", "symlink", "other"]
    size: int
    mod_time: str
    inode: int
    version: str

rename uses from_path/to_path because from is a Python keyword.

Semantics

  • stat defaults to lstat, so a symlink reports as symlink; follow dereferences it.
  • version is an opaque freshness token (inode + sub-second mtime + size) for
    optimistic-concurrency guards: re-stat before writing and compare for equality.
    Do not parse it.
  • Ranged read is selected by the presence of length, not its value — an explicit
    length: 0 reads zero bytes rather than falling back to the whole file. The server
    rejects a length above 8 MiB and 400s malformed/negative range params.
  • remove needs recursive for any directory (rm refuses directories without
    -r, empty or not); removing an absent path succeeds.
  • EphemeralBox picks all of this up — it shares box.files.

Example

// guarded edit: don't clobber a concurrent change
const before = await box.files.stat("src/app.ts");
const updated = (await box.files.read("src/app.ts")).replace("3000", "8080");
if ((await box.files.stat("src/app.ts")).version !== before.version) {
  throw new Error("changed underneath us — re-read and retry");
}
await box.files.write({ path: "src/app.ts", content: updated });

// read the head of a large log instead of pulling it whole
const head = await box.files.read("build.log", { length: 64 * 1024 });

Testing

  • JS: 408 unit tests, tsc + build clean.
  • Python: 201 unit tests, ruff, mypy, and the JS↔Python parity gate; sync client
    regenerated from the async source and regeneration-stable.
  • Verified against the live dev backend (coordinator v126 + agent on DX-2944):
    15 JS checks, 12 Python async, 6 Python sync — covering mkdir/stat/rename/remove,
    lstat vs follow, ranged reads, length: 0, the 400s, and 404s. An in-place
    same-size rewrite produced …1787156483.243568408-4…1787156483.536559672-4,
    confirming the version token detects changes that second-resolution mtime misses.

Release

  • npm: changeset included (patch).
  • PyPI: versions independently — this bumps upstash-box to 0.3.0 and promotes the
    CHANGELOG heading. That release also carries the previously unreleased browser
    (Stagehand v4 act replay, tab.run() removal), recordings download, shallow clone,
    and schedule update work. No tag is pushed here — tagging python-sdk-v0.3.0 is
    what publishes.

Important

Merge and deploy box-backend #237 to production before publishing either SDK —
these methods call endpoints that do not exist there yet.

Client support for the file-metadata endpoints (backend: DX-2944), in both SDKs
so the published surface stays at parity.

- `files.stat(path, { follow })` / `files.stat(path, follow=...)` — returns type
  (file/directory/symlink/other), size, mtime, inode, and an opaque `version`
  token for optimistic-concurrency guards. Defaults to lstat so a symlink is
  reported as such; `follow` dereferences it.
- `files.mkdir(path, { parents })`, `files.rename(from, to)`,
  `files.remove(path, { recursive })`.
- `files.read(path, { offset, length })` — bounded byte-range read. The range is
  selected by the presence of `length`, not its value, so an explicit length of 0
  reads zero bytes instead of falling back to the whole file.

Python mirrors the JS surface (async source of truth, sync client regenerated);
ruff, mypy, the JS<->Python parity gate, and both test suites pass.
Cuts the accumulated Unreleased work as 0.3.0: the filesystem metadata
operations added here, plus the previously unreleased browser (Stagehand v4
`act` replay, `tab.run()` removal), recordings download, shallow clone,
schedule update, and model-constant changes.

Bumps `pyproject.toml` and `upstash_box/_version.py`, promotes the CHANGELOG
heading, and records the JS parity point in RELEASE.md. Tagging
`python-sdk-v0.3.0` triggers the PyPI release, so that should wait until the
backend file-metadata endpoints are in production.
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

DX-2944

- prettier: reformat box-files.test.ts (JS ci:lint runs `prettier --check`).
- ruff B017: assert `ValidationError` instead of a blind `Exception` in the
  FileStat closed-set test.
- ruff I001: sort the test imports.

The earlier runs only linted `upstash_box/`, so the test-directory findings and
prettier were missed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds filesystem metadata operations and ranged reads across the JavaScript and Python SDKs while maintaining API parity.

Changes:

  • Adds stat, mkdir, rename, remove, and ranged read.
  • Introduces and exports FileStat.
  • Updates unit tests, parity documentation, and release metadata.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
.changeset/file-metadata-ops.md Records the JavaScript patch release.
packages/sdk/src/types.ts Defines FileStat.
packages/sdk/src/index.ts Exports FileStat.
packages/sdk/src/client.ts Implements the new filesystem APIs.
packages/sdk/src/__tests__/box-files.test.ts Adds JavaScript unit tests.
packages/python-sdk/upstash_box/types.py Defines Python FileStat.
packages/python-sdk/upstash_box/_version.py Bumps the Python version.
packages/python-sdk/upstash_box/_async/client.py Implements async filesystem APIs.
packages/python-sdk/upstash_box/_sync/client.py Mirrors the generated sync APIs.
packages/python-sdk/upstash_box/__init__.py Exports Python FileStat.
packages/python-sdk/tests/_async/test_box_files.py Adds async unit tests.
packages/python-sdk/RELEASE.md Records SDK release parity.
packages/python-sdk/pyproject.toml Bumps the package version.
packages/python-sdk/PARITY.md Documents filesystem API parity.
packages/python-sdk/CHANGELOG.md Documents the Python release.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +168 to +169
@respx.mock
async def test_read_file_range():
Comment on lines +109 to +110
describe("files.read range", () => {
it("sends offset and length for a bounded read", async () => {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants