feat(client): add server_info() method returning the full VersionInfo model - #1297
feat(client): add server_info() method returning the full VersionInfo model#1297Harsh23Kashyap wants to merge 1 commit into
Conversation
… model QdrantClient, AsyncQdrantClient, QdrantRemote, AsyncQdrantRemote, QdrantLocal, and AsyncQdrantLocal all gain a public server_info() method that returns the qdrant_client.http.models.VersionInfo pydantic model (with title, version, and commit). Remote mode calls the existing service_api.root() binding and returns the VersionInfo, or None on any failure (timeout, connection refused, non-2xx, API error). Local mode returns a synthetic VersionInfo with title='qdrant-client (local mode)', version=<client library version>, and commit=None. The method never raises; failures fold into None. Same best-effort pattern as health_check() and qdrant_version() from #1289, #1294. The AST transformer pipeline can't reproduce the async def version of server_info (the sync method gets kept sync because the method isn't in AsyncQdrantBase, but the async remote/facade need async def to await the underlying root() call). Added server_info to exclude_methods in client_generator.py and remote_generator.py; tools/generate_async_client.sh now has an AST-based post-regen step that re-injects the async def server_info into both async_qdrant_client.py and async_qdrant_remote.py. Fails loudly with non-zero exit if close() can't be found in the regenerated file (signature change), so a stale regen is caught immediately. Tests in tests/test_server_info.py cover sync and async facade, sync and async remote, sync local mode (returns synthetic VersionInfo with title='qdrant-client (local mode)'), async local mode, success and failure paths, connection errors, timeouts, attribute errors, runtime errors, the VersionInfo return type contract, and the never-raises contract. 22/22 new tests pass; 46/46 existing tests in test_tracing.py, test_common.py, test_in_memory.py, and test_local_persistence.py still pass (68 total). Fixes #1296
✅ Deploy Preview for poetic-froyo-8baba7 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughAdds Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
qdrant_client/async_qdrant_remote.py (1)
264-275: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBlind
except Exception:flagged by Ruff (BLE001).Same as
qdrant_remote.py— intentional for best-effort semantics, but worth a# noqa: BLE001with justification for lint cleanliness.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@qdrant_client/async_qdrant_remote.py` around lines 264 - 275, Update the exception handler in server_info to add a targeted Ruff suppression for BLE001, with a brief inline justification that all failures are intentionally converted to None for best-effort semantics. Preserve the existing async root request and return behavior.Source: Linters/SAST tools
qdrant_client/qdrant_remote.py (1)
324-335: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBlind
except Exception:flagged by Ruff (BLE001).Intentional here for the "never raises" contract, but Ruff will flag it (same pattern as
async_qdrant_remote.py). Consider a# noqa: BLE001with justification to keep lint clean, matching the PR's "maintain... ruff cleanliness" goal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@qdrant_client/qdrant_remote.py` around lines 324 - 335, Update the exception handler in server_info to retain the intentional catch-all required by its never-raises contract while suppressing Ruff BLE001 with an inline noqa annotation and brief justification. Keep returning None for every failure and align the suppression style with the corresponding async implementation.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@qdrant_client/async_qdrant_remote.py`:
- Around line 264-275: Update AsyncQdrantRemote.server_info() to check
_prefer_grpc and call self.grpc_root.HealthCheck(...) when enabled, while
retaining the existing REST root() call as the fallback and returning None on
failures. Apply the same branching behavior to the synchronous
QdrantRemote.server_info() implementation, mirroring info()’s gRPC usage.
In `@qdrant_client/qdrant_client.py`:
- Around line 180-192: The QdrantBase type does not expose the server_info
method used by the facade. Add a server_info declaration to QdrantBase with the
VersionInfo-or-None return type and the existing best-effort contract, ensuring
self._client.server_info() type-checks without changing runtime behavior.
In `@qdrant_client/qdrant_remote.py`:
- Around line 324-335: Update QdrantRemote.server_info() to use
self.grpc_root.HealthCheck when prefer_grpc=True, matching the branch used by
QdrantRemote.info(), while retaining the REST service_api.root() path otherwise.
Preserve the method’s None-on-failure behavior and mirror info()’s
synchronous/asynchronous handling consistently.
In `@tests/test_server_info.py`:
- Around line 29-33: Update both local-storage tests, including
test_local_path_returns_synthetic and its async counterpart, to accept pytest’s
tmp_path fixture and pass a unique path derived from it to QdrantClient instead
of hardcoded /tmp paths. Preserve the existing assertions and async behavior
while ensuring each test uses isolated temporary storage.
In `@tools/generate_async_client.sh`:
- Around line 36-101: Update the regeneration script’s injection logic for
AsyncQdrantClient.server_info so the generated async_qdrant_client.py also
contains the inspect import required by inspect.iscoroutine. Add the import
through a stable existing top-level import anchor or locate the first
import/from statement with the parsed AST, while preserving the existing method
injection behavior.
---
Nitpick comments:
In `@qdrant_client/async_qdrant_remote.py`:
- Around line 264-275: Update the exception handler in server_info to add a
targeted Ruff suppression for BLE001, with a brief inline justification that all
failures are intentionally converted to None for best-effort semantics. Preserve
the existing async root request and return behavior.
In `@qdrant_client/qdrant_remote.py`:
- Around line 324-335: Update the exception handler in server_info to retain the
intentional catch-all required by its never-raises contract while suppressing
Ruff BLE001 with an inline noqa annotation and brief justification. Keep
returning None for every failure and align the suppression style with the
corresponding async implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d5cf5fcd-1a4e-4d52-b300-2c96fc87b37d
📒 Files selected for processing (10)
qdrant_client/async_qdrant_client.pyqdrant_client/async_qdrant_remote.pyqdrant_client/local/async_qdrant_local.pyqdrant_client/local/qdrant_local.pyqdrant_client/qdrant_client.pyqdrant_client/qdrant_remote.pytests/test_server_info.pytools/async_client_generator/client_generator.pytools/async_client_generator/remote_generator.pytools/generate_async_client.sh
| async def server_info(self) -> types.VersionInfo | None: | ||
| """Return the Qdrant server's full ``VersionInfo`` model (with | ||
| ``title``, ``version``, and ``commit``). Awaits the REST ``/`` | ||
| endpoint via the existing async service API binding. | ||
|
|
||
| On any failure (timeout, connection refused, non-2xx, API | ||
| error), returns ``None`` rather than raising.""" | ||
| try: | ||
| return await self.http.service_api.root() | ||
| except Exception: | ||
| return None | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the relevant async/sync remote files and nearby implementations.
for f in qdrant_client/async_qdrant_remote.py qdrant_client/qdrant_remote.py; do
if [ -f "$f" ]; then
echo "===== $f ====="
wc -l "$f"
ast-grep outline "$f" --match "server_info" --view expanded || true
ast-grep outline "$f" --match "info" --view expanded || true
sed -n '240,285p' "$f"
sed -n '2530,2590p' "$f"
fi
done
# Search for server_info/root/HealthCheck implementations/usages across the package.
echo "===== server_info usages/definitions ====="
rg -n "def server_info|async def server_info|service_api\.root|HealthCheck|prefer_grpc|_prefer_grpc|convert_health_check_reply" qdrant_client -SRepository: qdrant/qdrant-client
Length of output: 20969
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "===== qdrant_client/qdrant_remote.py server_info and info ====="
sed -n '320,336p' qdrant_client/qdrant_remote.py
sed -n '2764,2782p' qdrant_client/qdrant_remote.py
echo "===== qdrant_client/async_qdrant_remote.py init/http/grpc attributes ====="
sed -n '45,75p' qdrant_client/async_qdrant_remote.py
sed -n '220,280p' qdrant_client/async_qdrant_remote.py
sed -n '430,455p' qdrant_client/async_qdrant_remote.py
sed -n '2520,2579p' qdrant_client/async_qdrant_remote.py
echo "===== qdrant_client/async_qdrant_remote.py attribute references around server_info ====="
rg -n "self\.(http|rest|grpc_root)\." qdrant_client/async_qdrant_remote.py -SRepository: qdrant/qdrant-client
Length of output: 11266
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "===== qdrant_client/async_qdrant_remote.py self.rest assignments ====="
rg -n "self\.(rest|http)\.service_api|asyncopenapiclient|service_api|REST|rest =" qdrant_client/async_qdrant_remote.py -S
echo "===== qdrant_client/qdrant_remote.py self.rest assignments ====="
rg -n "self\.(rest|openapi_client)\.service_api|service_api|REST|rest =" qdrant_client/qdrant_remote.py -S
echo "===== parent openapi_client/service_api definitions ====="
rg -n "class OpenAPI|openapi_client|service_api|class AsyncOpenAPIClient" qdrant_client/async_qdrant_remote.py qdrant_client/qdrant_remote.py qdrant_client -g '*.py' -SRepository: qdrant/qdrant-client
Length of output: 18447
Use gRPC in server_info() when _prefer_grpc is set.
AsyncQdrantRemote.server_info() unconditionally calls self.http.service_api.root(), so clients configured for gRPC (prefer_grpc=True) silently receive None instead of the health-check version. Mirror info() by using self.grpc_root.HealthCheck(...) for gRPC, with the REST fallback in the catch/else path where appropriate. The sync QdrantRemote.server_info() has the same _prefer_grpc gap.
🧰 Tools
🪛 Ruff (0.16.0)
[warning] 273-273: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@qdrant_client/async_qdrant_remote.py` around lines 264 - 275, Update
AsyncQdrantRemote.server_info() to check _prefer_grpc and call
self.grpc_root.HealthCheck(...) when enabled, while retaining the existing REST
root() call as the fallback and returning None on failures. Apply the same
branching behavior to the synchronous QdrantRemote.server_info() implementation,
mirroring info()’s gRPC usage.
| def server_info(self) -> types.VersionInfo | None: | ||
| """Return the Qdrant server's full ``VersionInfo`` model (with | ||
| ``title``, ``version``, and ``commit``). For local mode, returns | ||
| a synthetic ``VersionInfo`` with | ||
| ``title="qdrant-client (local mode)"``, | ||
| ``version=<client library version>``, and ``commit=None``. | ||
|
|
||
| On any remote failure (timeout, connection refused, non-2xx), | ||
| returns ``None`` rather than raising — the model is best-effort.""" | ||
| if hasattr(self, "_client"): | ||
| return self._client.server_info() | ||
| return None | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -a 'qdrant_base.py|client_base.py' qdrant_client --regex
rg -n 'class (Qdrant|AsyncQdrant)Base' -A 5 qdrant_client
rg -n 'def server_info' qdrant_clientRepository: qdrant/qdrant-client
Length of output: 1498
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- client_base.py ---\n'
sed -n '1,120p' qdrant_client/client_base.py
printf '\n--- qdrant_client.py self._client/server_info slices ---\n'
rg -n 'self\._client|class QdrantClient|def __init__|def server_info' qdrant_client/qdrant_client.py
printf '\n--- __init__.py exports ---\n'
sed -n '1,160p' qdrant_client/__init__.py
printf '\n--- check declared base methods/annotations for QdrantBase ---\n'
python3 - <<'PY'
import ast
from pathlib import Path
path = Path("qdrant_client/client_base.py")
tree = ast.parse(path.read_text())
cls = next((node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "QdrantBase"), None)
assert cls
methods = [m.name for m in cls.body if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef))]
print("QdrantBase methods:", methods)
print("server_info declared:", "server_info" in methods)
print("has annotations section:", any(isinstance(node, ast.AnnAssign) or isinstance(node, ast.Assign) for node in cls.body))
PYRepository: qdrant/qdrant-client
Length of output: 249
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- client_base.py ---'
sed -n '1,120p' qdrant_client/client_base.py
echo
echo '--- qdrant_client.py self._client/server_info slices ---'
rg -n 'self\._client|class QdrantClient|def __init__|def server_info' qdrant_client/qdrant_client.py
echo
echo '--- __init__.py exports ---'
sed -n '1,160p' qdrant_client/__init__.py
echo
echo '--- check declared base methods/annotations for QdrantBase ---'
python3 - <<'PY'
import ast
from pathlib import Path
path = Path("qdrant_client/client_base.py")
tree = ast.parse(path.read_text())
cls = next((node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "QdrantBase"), None)
assert cls
methods = [m.name for m in cls.body if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef))]
print("QdrantBase methods:", methods)
print("server_info declared:", "server_info" in methods)
print("has annotations section:", any(isinstance(node, ast.AnnAssign) or isinstance(node, ast.Assign) for node in cls.body))
PYRepository: qdrant/qdrant-client
Length of output: 9774
Declare server_info on QdrantBase.
self._client is typed as QdrantBase, but QdrantBase does not declare server_info, so type checkers can flag self._client.server_info() in this facade. Add the stub method to the base type or adjust the _client type so this access is visible.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@qdrant_client/qdrant_client.py` around lines 180 - 192, The QdrantBase type
does not expose the server_info method used by the facade. Add a server_info
declaration to QdrantBase with the VersionInfo-or-None return type and the
existing best-effort contract, ensuring self._client.server_info() type-checks
without changing runtime behavior.
| def server_info(self) -> types.VersionInfo | None: | ||
| """Return the Qdrant server's full ``VersionInfo`` model (with | ||
| ``title``, ``version``, and ``commit``). Calls the REST ``/`` | ||
| endpoint via the existing service API binding. | ||
|
|
||
| On any failure (timeout, connection refused, non-2xx, API | ||
| error), returns ``None`` rather than raising.""" | ||
| try: | ||
| return self.openapi_client.service_api.root() | ||
| except Exception: | ||
| return None | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)qdrant_remote\.py$|async_qdrant_remote\.py$' || true
echo "== qdrant_remote outline relevant =="
ast-grep outline qdrant_client/qdrant_remote.py --match server_info --view expanded || true
ast-grep outline qdrant_client/qdrant_remote.py --match info --view expanded || true
echo "== server_info and info sections =="
sed -n '300,345p' qdrant_client/qdrant_remote.py
sed -n '2740,2805p' qdrant_client/qdrant_remote.py
echo "== prefer_grpc occurrences in qdrant_remote =="
rg -n "_prefer_grpc|prefer_grpc|grpc_root|HealthCheck|service_api\.root" qdrant_client/qdrant_remote.py
echo "== async equivalent if present =="
if [ -f qdrant_client/qdrant_async_remote.py ]; then
sed -n '300,345p' qdrant_client/qdrant_async_remote.py
sed -n '2740,2805p' qdrant_client/qdrant_async_remote.py
fi
echo "== imports and conversion references =="
rg -n "from .* GrpcToRest|class GrpcToRest|convert_health_check_reply|HealthCheckRequest|grpc" qdrant_client -g '*.py' | head -n 200Repository: qdrant/qdrant-client
Length of output: 29152
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== search remote client constructors for prefer_grpc semantics =="
rg -n "prefer_grpc|_prefer_grpc" qdrant_client qdrant_client/**/*.py | head -n 240
echo "== inspect qdrant_client init signatures and client wrappers =="
rg -n "class QdrantClient|prefer_grpc|QdrantRemote\\(" qdrant_client -g '*.py' | head -n 240
sed -n '1,220p' qdrant_client/qdrant_client.py 2>/dev/null || trueRepository: qdrant/qdrant-client
Length of output: 24175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== async_qdrant_client server_info section =="
rg -n "def server_info|server_info\\(" qdrant_client/async_qdrant_client.py
sed -n '220,270p' qdrant_client/async_qdrant_client.py
echo "== async_qdrant_remote info/server_info section =="
sed -n '300,340p' qdrant_client/async_qdrant_remote.py
sed -n '2620,2690p' qdrant_client/async_qdrant_remote.py
echo "== read-only behavior probe for QdrantRemote path selection =="
python3 - <<'PY'
from pathlib import Path
p = Path('qdrant_client/qdrant_remote.py')
s = p.read_text()
idx_server_info = s.index(' def server_info(self)')
idx_info = s.index(' def info(self) -> types.VersionInfo:')
server = s[idx_server_info:idx_info].splitlines()
info = []
for i, line in enumerate(s[idx_info:].splitlines()[:35], start=idx_info):
info.append((i+1, line))
print("server_info contains _prefer_grpc:", any("_prefer_grpc" in line for line in server))
print("server_info REST-only path:", any("openapi_client.service_api.root()" in line for line in server))
print("info contains gRPC branch:", any("_prefer_grpc" in line for line in s[idx_info:idx_info+s[idx_info:].index(" version_info = self.rest.service_api.root()")].splitlines()))
PYRepository: qdrant/qdrant-client
Length of output: 7487
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== precise server_info call in sync client =="
rg -n -C 8 " def server_info" qdrant_client/qdrant_client.py
echo "== precise server_info call in async client =="
rg -n -C 8 " async def server_info" qdrant_client/async_qdrant_client.py
echo "== precise server_info in qdrant_remote =="
sed -n '298,336p' qdrant_client/qdrant_remote.py
echo "== precise info and root conversions =="
sed -n '2766,2786p' qdrant_client/qdrant_remote.py
sed -n '2632,2644p' qdrant_client/qdrant_remote.pyRepository: qdrant/qdrant-client
Length of output: 4669
Use HealthCheck for server_info() when prefer_grpc=True.
QdrantRemote.info() uses gRPC HealthCheck via self.grpc_root when prefer_grpc=True, but QdrantRemote.server_info() always calls the REST openapi_client.service_api.root(). Clients with prefer_grpc=True can fail this path even though gRPC HealthCheck would succeed, then silently return None; mirror the info() branch here and apply the same async behavior consistently.
🧰 Tools
🪛 Ruff (0.16.0)
[warning] 333-333: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@qdrant_client/qdrant_remote.py` around lines 324 - 335, Update
QdrantRemote.server_info() to use self.grpc_root.HealthCheck when
prefer_grpc=True, matching the branch used by QdrantRemote.info(), while
retaining the REST service_api.root() path otherwise. Preserve the method’s
None-on-failure behavior and mirror info()’s synchronous/asynchronous handling
consistently.
| def test_local_path_returns_synthetic(self): | ||
| client = QdrantClient(path="/tmp/qdrant_server_info_test") | ||
| result = client.server_info() | ||
| assert result is not None | ||
| assert result.title == "qdrant-client (local mode)" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use tmp_path fixture instead of hardcoded /tmp paths.
Both tests use fixed /tmp/qdrant_server_info_test[_async] paths for local storage with no cleanup. Re-running the suite (or parallel workers reusing the same path) can hit a stale storage/lock-file from a previous run, causing flaky failures; the path is also non-portable outside POSIX systems.
🧹 Proposed fix using pytest's `tmp_path` fixture
- def test_local_path_returns_synthetic(self):
- client = QdrantClient(path="/tmp/qdrant_server_info_test")
+ def test_local_path_returns_synthetic(self, tmp_path):
+ client = QdrantClient(path=str(tmp_path / "qdrant_server_info_test"))
result = client.server_info()
assert result is not None
assert result.title == "qdrant-client (local mode)"- async def test_local_path_returns_synthetic(self):
- client = AsyncQdrantClient(path="/tmp/qdrant_server_info_test_async")
+ async def test_local_path_returns_synthetic(self, tmp_path):
+ client = AsyncQdrantClient(path=str(tmp_path / "qdrant_server_info_test_async"))
result = await client.server_info()
assert result is not None
assert result.title == "qdrant-client (local mode)"Also applies to: 73-77
🧰 Tools
🪛 ast-grep (0.45.0)
[info] 29-29: Do not hardcode temporary file or directory names
Context: "/tmp/qdrant_server_info_test"
Note: [CWE-377] Insecure Temporary File.
(hardcoded-tmp-file)
🪛 Ruff (0.16.0)
[error] 30-30: Probable insecure usage of temporary file or directory: "/tmp/qdrant_server_info_test"
(S108)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_server_info.py` around lines 29 - 33, Update both local-storage
tests, including test_local_path_returns_synthetic and its async counterpart, to
accept pytest’s tmp_path fixture and pass a unique path derived from it to
QdrantClient instead of hardcoded /tmp paths. Preserve the existing assertions
and async behavior while ensuring each test uses isolated temporary storage.
Source: Linters/SAST tools
| python3 - "$ABSOLUTE_PROJECT_ROOT" <<'PY' | ||
| import sys, ast, pathlib | ||
|
|
||
| root = pathlib.Path(sys.argv[1]) | ||
| targets = [ | ||
| ("qdrant_client/async_qdrant_client.py", """\ | ||
| async def server_info(self) -> types.VersionInfo | None: | ||
| \"\"\"Return the Qdrant server's full ``VersionInfo`` model (with | ||
| ``title``, ``version``, and ``commit``). For local mode, returns | ||
| a synthetic ``VersionInfo``. On any remote failure, returns | ||
| ``None`` rather than raising \u2014 the model is best-effort. | ||
|
|
||
| The inner client may return a coroutine (remote mode) or a | ||
| model (local mode); await the coroutine when present.\"\"\" | ||
| if hasattr(self, \"_client\"): | ||
| result = self._client.server_info() | ||
| if inspect.iscoroutine(result): | ||
| result = await result | ||
| return result | ||
| return None | ||
| """), | ||
| ("qdrant_client/async_qdrant_remote.py", """\ | ||
| async def server_info(self) -> types.VersionInfo | None: | ||
| \"\"\"Return the Qdrant server's full ``VersionInfo`` model (with | ||
| ``title``, ``version``, and ``commit``). Awaits the REST ``/`` | ||
| endpoint via the existing async service API binding. | ||
|
|
||
| On any failure (timeout, connection refused, non-2xx, API | ||
| error), returns ``None`` rather than raising.\"\"\" | ||
| try: | ||
| return await self.http.service_api.root() | ||
| except Exception: | ||
| return None | ||
| """), | ||
| ] | ||
|
|
||
| for rel, inject in targets: | ||
| p = root / rel | ||
| if not p.exists(): | ||
| continue | ||
| text = p.read_text() | ||
| if "async def server_info" in text: | ||
| continue | ||
| # Find the close() method to insert after it. | ||
| tree = ast.parse(text) | ||
| close_node = None | ||
| for node in tree.body: | ||
| if isinstance(node, ast.ClassDef): | ||
| for item in node.body: | ||
| if ( | ||
| isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) | ||
| and item.name == "close" | ||
| ): | ||
| close_node = item | ||
| break | ||
| if close_node is not None: | ||
| break | ||
| if close_node is None: | ||
| print(f"regen: {rel}: close() not found; async def server_info NOT re-injected (manual fix required)") | ||
| sys.exit(1) | ||
| lines = text.splitlines(keepends=True) | ||
| insert_at = close_node.end_lineno | ||
| new_text = "".join(lines[:insert_at]) + "\n" + inject + "".join(lines[insert_at:]) | ||
| p.write_text(new_text) | ||
| print(f"regen: injected async def server_info into {rel}") | ||
| PY |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Regeneration will drop import inspect, breaking AsyncQdrantClient.server_info() on next run.
The injected async_qdrant_client.py snippet (lines 42-56) references inspect.iscoroutine(result), but this script never re-injects import inspect into the file. Since client_generator.py excludes server_info from transformation, the freshly generated async_qdrant_client.py (produced by the mv at line 18, before this injection block runs) contains neither the method nor the import — qdrant_client.py (the sync source) doesn't import inspect either. On the very next regeneration, this will silently ship a server_info() that raises NameError: name 'inspect' is not defined on every call (for both local and remote clients), since that line runs unconditionally once hasattr(self, "_client") is true.
🔧 Proposed fix: also ensure the import is present
for rel, inject in targets:
p = root / rel
if not p.exists():
continue
text = p.read_text()
if "async def server_info" in text:
continue
+ if rel.endswith("async_qdrant_client.py") and "import inspect" not in text:
+ text = text.replace("import warnings\n", "import inspect\nimport warnings\n", 1)
# Find the close() method to insert after it.
- tree = ast.parse(text)
+ tree = ast.parse(text)(Adjust the anchor import line to whatever stable top-level import already exists in the generated file, or insert before the first import/from statement found via ast.parse.)
🔍 Verification script
#!/bin/bash
echo "--- Does sync qdrant_client.py import inspect? ---"
grep -n '^import inspect' qdrant_client/qdrant_client.py && echo FOUND || echo "NOT FOUND (confirms gap)"
echo "--- Current async_qdrant_client.py import block ---"
sed -n '1,26p' qdrant_client/async_qdrant_client.py🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tools/generate_async_client.sh` around lines 36 - 101, Update the
regeneration script’s injection logic for AsyncQdrantClient.server_info so the
generated async_qdrant_client.py also contains the inspect import required by
inspect.iscoroutine. Add the import through a stable existing top-level import
anchor or locate the first import/from statement with the parsed AST, while
preserving the existing method injection behavior.
Summary
Add a public
server_info()method to all 6 client classes so users can read the Qdrant server's fullVersionInfo(withtitle,version,commit) without digging intoclient.http.service_api.root().Fixes #1296.
Problem
qdrant_version()(#1295) returns just the version string (e.g."1.10.0"). The fullVersionInfomodel has two more fields that the user might want:title(the server's display name) andcommit(the git commit hash). The infrastructure is in place —service_api.root()returns the fullm.VersionInfopydantic model — but the high-level client classes don't expose it.Implementation
One
server_info()method per class. Each is 1-5 lines, all return aVersionInfo | None. No new abstractions, no new state, no public API change beyond the new method names.QdrantRemote.server_info()— sync. Callsself.openapi_client.service_api.root(). Returns theVersionInfo, orNoneon any exception.AsyncQdrantRemote.server_info()— async. Awaitsself.http.service_api.root(). Returns theVersionInfo, orNoneon any exception.QdrantLocal.server_info()— sync. Returns a syntheticVersionInfo(title="qdrant-client (local mode)", version=<client library version>, commit=None). Never fails.AsyncQdrantLocal.server_info()— sync (no I/O). Same asQdrantLocal.server_info().QdrantClient.server_info()— sync. Delegates toself._client.server_info().AsyncQdrantClient.server_info()— async. Awaits the inner result if it's a coroutine (remote mode), returns the model directly (local mode). Detected viainspect.iscoroutine.The
Nonecontract: any failure (timeout, connection refused, non-2xx, API error) is folded into aNonereturn; no exception escapes. Same "best-effort" pattern ashealth_check()(#1289) andqdrant_version()(#1294).The return type is
qdrant_client.http.models.VersionInfo | None. Users who want just the version string continue to useqdrant_version()(from #1295); users who want the full model useserver_info().Regen
The async files are generated by the AST transformer pipeline. The transformer converts
def Xtoasync def Xonly whenXis inasync_methods(built fromiscoroutinefunctionof the async base class).server_infois not in any async base and is not a coroutine function, so the transformer would keep it sync in the async mirror — but the async remote and async facade need it to beasync def(to await the underlying root call).Two changes keep the new methods alive across regens:
server_infoadded toexclude_methodsin bothclient_generator.pyandremote_generator.py. The transformer skips it during regen.tools/generate_async_client.shgets an AST-based post-regen step that re-injects theasync def server_infointo bothasync_qdrant_client.pyandasync_qdrant_remote.pyafter theclose()method. Fails loudly with non-zero exit ifclose()cannot be found (signature change), so a stale regen is caught immediately.The local async file (
local/async_qdrant_local.py) keepsdef server_info(sync) because the AST transformer correctly leaves it sync when not inasync_methods— and the facade correctly detects that sync return withinspect.iscoroutineand returns it directly.Verified the sed step end-to-end by manually removing the methods from the generated files, running the regen script, and confirming the methods are re-injected correctly with content matching the originals (empty diff).
Tests
tests/test_server_info.py(22 tests) covers:QdrantClientfacade: local:memory:(syntheticVersionInfo), local path, remote with mockedroot(success / connection error / timeout).AsyncQdrantClientfacade: same matrix (allawaited).QdrantRemotedirectly: success, connection error, attribute error.AsyncQdrantRemotedirectly: success, connection error.QdrantLocaldirectly: syntheticVersionInfo, never returnsNone.AsyncQdrantLocaldirectly: same.RuntimeErrorfolds toNone.title/version/commitattributes (pydantic model).All 22 new tests pass; 46 existing tests in
test_tracing.py,test_common.py,test_in_memory.py, andtest_local_persistence.pystill pass (68 total).Verification
VersionInfousesimportlib.metadata.version("qdrant-client")(Python 3.8+ stdlib).Base branch
PR targets
upstream/devper maintainer joein's 2026-07-21 close comment on #1269: "All the PRs should pointdevbranch, not master." Matches the PR template at.github/PULL_REQUEST_TEMPLATE.md:4.Out of scope
commitfield as a separateqdrant_commit()method. Users can doclient.server_info().commitif they want.closedproperty on the facade classes (orthogonal; thewithblock from feat(client): support context manager on QdrantClient and AsyncQdrantClient #1286 covers the common case).VersionInfomodel only hastitle,version,commit; if more fields are added upstream, this method automatically returns them.