Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
path: coverage-${{ matrix.folder }}.xml

pyodide-e2e:
name: Run Pyodide (WASM) Unit + e2e Tests
name: Run Pyodide Unit + e2e Tests
runs-on: ubuntu-latest
timeout-minutes: 15
# The interpreter comes from the pyodide pin in ci/pyodide-e2e/package.json.
Expand All @@ -128,19 +128,13 @@ jobs:
with:
username: ${{secrets.DOCKER_USERNAME}}
password: ${{secrets.DOCKER_PASSWORD}}
- name: Build pure wheels (base client + grpc-web)
- name: Build weaviate-client and weaviate-client-web wheels
run: |
pip install build
python -m build --wheel --outdir dist .
python -m build --wheel --outdir dist packages/web
- name: Assert both wheels carry the same version
# weaviate-client-web is versioned in lockstep with weaviate-client: both derive
# from the same git tag via setuptools_scm.
run: |
base=$(basename dist/weaviate_client-*.whl); base=${base#weaviate_client-}; base=${base%%-*}
web=$(basename dist/weaviate_client_web-*.whl); web=${web#weaviate_client_web-}; web=${web%%-*}
echo "weaviate-client=$base weaviate-client-web=$web"
test "$base" = "$web"
run: ci/assert-lockstep.sh dist
- name: Run the unit suite inside Pyodide under Node
# weaviate-client-web imports pyodide, so its unit tests run only here.
# JSPI lets pytest run async tests.
Expand Down Expand Up @@ -354,7 +348,7 @@ jobs:
cache: 'pip' # caching pip dependencies
- name: Install dependencies
run: pip install -r requirements-test.txt -r requirements-devel.txt
- name: Build binary wheels (base client + grpc-web companion)
- name: Build weaviate-client and weaviate-client-web wheels
run: |
python -m build
python -m build --wheel --outdir dist packages/web
Expand Down Expand Up @@ -428,7 +422,7 @@ jobs:
cache: 'pip' # caching pip dependencies
- name: Install dependencies
run: pip install -r requirements-devel.txt
- name: Build distributions (base client + grpc-web companion)
- name: Build distributions (weaviate-client sdist+wheel, weaviate-client-web wheel)
# weaviate-client-web is wheel-only: its version comes from git tags, which an
# sdist lacks.
run: |
Expand All @@ -437,11 +431,7 @@ jobs:
- name: Assert both packages carry the same version
# weaviate-client-web pins weaviate-client==<own version>; a mismatch leaves the
# [grpc-web] extra unresolvable.
run: |
base=$(basename dist/weaviate_client-*.whl); base=${base#weaviate_client-}; base=${base%%-*}
web=$(basename dist/weaviate_client_web-*.whl); web=${web#weaviate_client_web-}; web=${web%%-*}
echo "weaviate-client=$base weaviate-client-web=$web"
test "$base" = "$web"
run: ci/assert-lockstep.sh dist
- name: Publish distributions 📦 to PyPI on new tags
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
Expand Down
12 changes: 12 additions & 0 deletions ci/assert-lockstep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Fails unless the weaviate-client and weaviate-client-web wheels in <dist-dir> (default:
# dist) carry the same version; the two packages are released in lockstep.
set -euo pipefail
dist="${1:-dist}"
base=$(basename "$dist"/weaviate_client-*.whl); base=${base#weaviate_client-}; base=${base%%-*}
web=$(basename "$dist"/weaviate_client_web-*.whl); web=${web#weaviate_client_web-}; web=${web%%-*}
echo "weaviate-client=$base weaviate-client-web=$web"
if [ "$base" != "$web" ]; then
echo "version mismatch: weaviate-client $base != weaviate-client-web $web (must release in lockstep)" >&2
exit 1
fi
2 changes: 1 addition & 1 deletion ci/pyodide-e2e/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "weaviate-pyodide-e2e",
"private": true,
"description": "Runs the weaviate-client e2e suite inside Pyodide (WASM) under Node",
"description": "Runs the weaviate-client-web unit and e2e suites inside Pyodide under Node",
"dependencies": {
"pyodide": "314.0.4"
}
Expand Down
26 changes: 6 additions & 20 deletions ci/pyodide-e2e/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,16 @@
// Usage: node run.mjs <wheels-dir> (one weaviate_client-*.whl, one weaviate_client_web-*.whl)
// Env: WEAVIATE_HOST (default localhost), WEAVIATE_PORT (default 8090).
// The pyodide npm pin in package.json fixes the interpreter.
import { readdirSync, readFileSync } from "node:fs";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { loadPyodide } from "pyodide";

if (!process.argv[2]) {
console.error("usage: node run.mjs <wheels-dir>");
process.exit(2);
}
const wheelsDir = resolve(process.argv[2]);
const here = dirname(fileURLToPath(import.meta.url));
import { wheelsFromArgv } from "./wheels.mjs";

const wheels = readdirSync(wheelsDir)
.filter((f) => f.endsWith(".whl"))
.sort(); // installs weaviate_client before weaviate_client_web, which depends on it
const prefixes = ["weaviate_client-", "weaviate_client_web-"];
if (
wheels.length !== 2 ||
!prefixes.every((p) => wheels.some((w) => w.startsWith(p)))
) {
console.error(
`expected exactly one weaviate_client-*.whl and one weaviate_client_web-*.whl in ${wheelsDir}, found: ${JSON.stringify(wheels)}`,
);
process.exit(2);
}
const { wheelsDir, wheels } = wheelsFromArgv("run.mjs");
const here = dirname(fileURLToPath(import.meta.url));

const pyodide = await loadPyodide({
env: {
Expand Down Expand Up @@ -69,3 +53,5 @@ try {
console.error(err);
process.exit(1);
}
// The interpreter keeps live handles on the Node event loop, so exit explicitly.
process.exit(0);
31 changes: 7 additions & 24 deletions ci/pyodide-e2e/units.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,15 @@
// bootstrap check. No Weaviate needed.
// Usage: node --experimental-wasm-jspi units.mjs <wheels-dir> (same wheels as run.mjs)
// JSPI is required: pytest runs async tests through run_until_complete (stack switching).
import { readdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { loadPyodide } from "pyodide";

if (!process.argv[2]) {
console.error("usage: node units.mjs <wheels-dir>");
process.exit(2);
}
const wheelsDir = resolve(process.argv[2]);
const here = dirname(fileURLToPath(import.meta.url));
import { wheelsFromArgv } from "./wheels.mjs";

const wheels = readdirSync(wheelsDir)
.filter((f) => f.endsWith(".whl"))
.sort(); // installs weaviate_client before weaviate_client_web, which depends on it
const prefixes = ["weaviate_client-", "weaviate_client_web-"];
if (
wheels.length !== 2 ||
!prefixes.every((p) => wheels.some((w) => w.startsWith(p)))
) {
console.error(
`expected exactly one weaviate_client-*.whl and one weaviate_client_web-*.whl in ${wheelsDir}, found: ${JSON.stringify(wheels)}`,
);
process.exit(2);
}
const { wheelsDir, wheels } = wheelsFromArgv("units.mjs");
const here = dirname(fileURLToPath(import.meta.url));

// Fresh interpreter with micropip ready and the wheels dir mounted.
async function freshPyodide() {
Expand All @@ -50,8 +33,8 @@ async function freshPyodide() {
pyodide.runPython(`
import sys
assert "weaviate_client_web" not in sys.modules
import weaviate # the ONLY weaviate-side import: must bootstrap the companion
assert "weaviate_client_web" in sys.modules, "hook did not import the companion"
import weaviate # first weaviate import: installs weaviate_client_web
assert "weaviate_client_web" in sys.modules, "hook did not import weaviate_client_web"
import weaviate_client_web
import grpc
import httpx
Expand All @@ -62,9 +45,9 @@ assert getattr(
httpx.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False
) is True
`);
console.log("OK scenario: bare 'import weaviate' bootstraps the companion");
console.log("OK scenario: bare 'import weaviate' bootstraps weaviate_client_web");
} catch (err) {
console.error("FAIL scenario: bare 'import weaviate' bootstraps the companion");
console.error("FAIL scenario: bare 'import weaviate' bootstraps weaviate_client_web");
console.error(err);
process.exit(1);
}
Expand Down
26 changes: 26 additions & 0 deletions ci/pyodide-e2e/wheels.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Shared by run.mjs and units.mjs: resolves <wheels-dir> (argv[2]) to exactly the two
// locally-built pure wheels, weaviate_client-*.whl and weaviate_client_web-*.whl.
import { readdirSync } from "node:fs";
import { resolve } from "node:path";

export function wheelsFromArgv(script) {
if (!process.argv[2]) {
console.error(`usage: node ${script} <wheels-dir>`);
process.exit(2);
}
const wheelsDir = resolve(process.argv[2]);
const wheels = readdirSync(wheelsDir)
.filter((f) => f.endsWith(".whl"))
.sort(); // installs weaviate_client before weaviate_client_web, which depends on it
const prefixes = ["weaviate_client-", "weaviate_client_web-"];
if (
wheels.length !== 2 ||
!prefixes.every((p) => wheels.some((w) => w.startsWith(p)))
) {
console.error(
`expected exactly one weaviate_client-*.whl and one weaviate_client_web-*.whl in ${wheelsDir}, found: ${JSON.stringify(wheels)}`,
);
process.exit(2);
}
return { wheelsDir, wheels };
}
17 changes: 10 additions & 7 deletions packages/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,16 @@ Weaviate Cloud: browser use requires **Allow all CORS origins** in the cluster's
in the Weaviate Cloud console (takes a few minutes to apply). Until it applies, the client
fails at its first REST call with a `Failed to fetch` connection error.

`use_async_with_custom()` still requires `grpc_host`/`grpc_port`/`grpc_secure` — Python
cannot drop required parameters on one platform the way TypeScript drops them from a
type. Pass the HTTP values; anything else is overridden with them and warned about
(`Con006`), so a browser client never silently points somewhere it cannot reach.
`use_async_with_custom()` still requires `grpc_host`/`grpc_port`/`grpc_secure`. Under
Pyodide they are ignored: gRPC always uses the REST endpoint, and values that differ from
the HTTP ones are reported in a `Con006` warning. Code that also runs on CPython should
keep its native gRPC values (there, the same host and port for both is an error) and may
ignore that warning.

```python
client = weaviate.use_async_with_custom(
http_host="localhost", http_port=8080, http_secure=False,
grpc_host="localhost", grpc_port=8080, grpc_secure=False, # = the HTTP endpoint
grpc_host="localhost", grpc_port=50051, grpc_secure=False, # CPython; ignored under Pyodide
)
```

Expand All @@ -106,7 +107,7 @@ Importing `weaviate_client_web` before `weaviate` is equivalent.
| Bulk insert: `collection.data.insert_many()` | unary gRPC | Yes, the bulk path under Pyodide |
| `batch.stream()` / `batch.experimental()` (BatchStream) | bidi streaming | No: grpc-web has no bidirectional streaming; raises at once, use `insert_many()` |
| `batch.dynamic()` / `fixed_size()` / `rate_limit()` | sync-client API | No: sync client only |
| Embedded Weaviate (`use_async_with_embedded`) | subprocess | No: raises "not supported under WebAssembly/Pyodide" |
| Embedded Weaviate (`use_async_with_embedded`) | subprocess | No: raises "not supported under Pyodide" |
| Synchronous client | — | No: async only |

## Configuration not honored in the browser
Expand Down Expand Up @@ -145,7 +146,9 @@ a bad API key) are reported as `INTERNAL: grpc-web response contained no message
instead of the real error.

In the browser a CORS-blocked request is indistinguishable from a network failure
(`TypeError: Failed to fetch`), and is retried as UNAVAILABLE.
(`TypeError: Failed to fetch`); the error text names CORS as one possible cause. On a
channel that has not yet received any response such a failure is reported as `UNKNOWN`
and fails at once; after the first response it is `UNAVAILABLE` and retried.

## Testing

Expand Down
7 changes: 2 additions & 5 deletions packages/web/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ build-backend = "setuptools.build_meta"

[project]
name = "weaviate-client-web"
description = "grpc-web / WASM (Pyodide) transport for the Weaviate Python client"
description = "grpc-web and fetch transports for the Weaviate Python client under Pyodide"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "BSD-3-Clause" }
authors = [{ name = "Weaviate", email = "hello@weaviate.io" }]
keywords = ["weaviate", "grpc-web", "pyodide", "wasm", "emscripten"]
# Version comes from the repository's git tags via setuptools_scm (root below), so every
# build carries the same version as weaviate-client; CI asserts the built wheels match.
# Dependencies are computed in setup.py: the weaviate-client requirement is pinned to
# that same version at build time, so mismatched pairs cannot resolve at install time.
# version: git tags (setuptools_scm); dependencies: setup.py (lockstep weaviate-client pin)
dynamic = ["version", "dependencies"]

[project.urls]
Expand Down
8 changes: 8 additions & 0 deletions packages/web/src/weaviate_client_web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
import os
import sys

try:
import pyodide # noqa: F401
except ImportError as exc:
raise ImportError(
"weaviate-client-web only works under Pyodide (sys.platform == 'emscripten'); "
"on CPython the weaviate-client package uses native gRPC and does not need it."
) from exc

from ._channel import GrpcWebChannel, set_sender
from ._httpx_fetch import (
install_fetch_transport,
Expand Down
Loading
Loading