feat(sdk): 实现 Python SDK 最小可用(Client + Adapter + GenericAdapter + Lea…#17
Conversation
…rningEngine) Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
This PR adds a minimal viable GeneHub Python SDK under packages/sdk/python, aligning core capabilities with the existing TypeScript SDK so Python-side agents can call the Registry API, install genes via an adapter, and run the learning-task workflow.
Changes:
- Introduces
GeneHubClient(Registry HTTP client), adapter abstractions (GeneAdapter) and a filesystem-basedGenericAdapter. - Adds a minimal
LearningEnginefor creating learning tasks and parsing learning results. - Adds Python packaging (pyproject/lockfile), unit tests, and updates repo docs to reflect Python SDK availability.
Reviewed changes
Copilot reviewed 15 out of 17 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/sdk/python/uv.lock | Adds a uv lockfile for the Python SDK dependencies. |
| packages/sdk/python/pyproject.toml | Defines Python package metadata, dependencies, Ruff + pytest config. |
| packages/sdk/python/README.md | Documents Python SDK purpose, structure, and usage examples. |
| packages/sdk/python/src/genehub_sdk/init.py | Exposes the public Python SDK surface (client/adapters/learning/types). |
| packages/sdk/python/src/genehub_sdk/types.py | Adds TypedDict-based models for manifests, API responses, adapter I/O. |
| packages/sdk/python/src/genehub_sdk/client.py | Implements GeneHubClient + GeneHubError for Registry API calls. |
| packages/sdk/python/src/genehub_sdk/adapters/init.py | Exports adapter types and the generic adapter. |
| packages/sdk/python/src/genehub_sdk/adapters/base.py | Defines the GeneAdapter abstract interface. |
| packages/sdk/python/src/genehub_sdk/adapters/generic.py | Implements a filesystem-based GenericAdapter. |
| packages/sdk/python/src/genehub_sdk/learning/init.py | Exports LearningEngine. |
| packages/sdk/python/src/genehub_sdk/learning/engine.py | Minimal learning-task creation + result parsing implementation. |
| packages/sdk/python/tests/test_client.py | Tests GeneHubClient using pytest-httpx mocking. |
| packages/sdk/python/tests/test_generic_adapter.py | Tests GenericAdapter install/uninstall/list/version behavior. |
| packages/sdk/python/tests/test_learning_engine.py | Tests learning task creation + result parsing behavior. |
| docs/architecture.md | Updates architecture docs to mark Python SDK as minimally implemented and lists its tree. |
| README.md | Updates root README to list the Python SDK package/path. |
| .gitignore | Adds common Python virtualenv/cache ignores. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from genehub_sdk import GeneHubClient | ||
|
|
||
| client = GeneHubClient(base_url="https://registry.genehub.dev", token="ghb_xxx") | ||
| genes = client.search_genes(q="code") |
There was a problem hiding this comment.
README 示例里调用了 client.search_genes(q="code"),但 GeneHubClient.search_genes 的参数名是 query(或直接用第一个位置参数)。按当前示例会触发 TypeError: unexpected keyword argument 'q';建议将示例改为 client.search_genes("code") 或 client.search_genes(query="code")。
| genes = client.search_genes(q="code") | |
| genes = client.search_genes(query="code") |
| qs = "&".join(f"{k}={v}" for k, v in params) | ||
| path = f"/api/v1/genes?{qs}" if qs else "/api/v1/genes" | ||
| result = self._request("GET", path) |
There was a problem hiding this comment.
search_genes 这里手动用 "&".join(f"{k}={v}") 拼查询字符串,没有做 URL 编码;当 query/tags/category 等包含空格、&、?、非 ASCII 等字符时会生成非法或语义错误的 URL。建议改为使用 httpx 的 params=(或 urllib.parse.urlencode)让库负责编码。
| qs = "&".join(f"{k}={v}" for k, v in params) | |
| path = f"/api/v1/genes?{qs}" if qs else "/api/v1/genes" | |
| result = self._request("GET", path) | |
| result = self._request("GET", "/api/v1/genes", params=params) |
| def _request(self, method: str, path: str, **kwargs: Any) -> Any: | ||
| url = f"{self._base_url}{path}" | ||
| with httpx.Client(timeout=30.0) as client: | ||
| resp = client.request( | ||
| method, | ||
| url, | ||
| headers=self._headers(), | ||
| **kwargs, | ||
| ) | ||
| data = resp.json() | ||
| body = data if isinstance(data, dict) else {} | ||
| code = body.get("code", -1) | ||
| if not resp.is_success or code != 0: | ||
| msg = body.get("message") or f"HTTP {resp.status_code}" | ||
| err_code = body.get("error_code") | ||
| raise GeneHubError(msg, error_code=err_code) |
There was a problem hiding this comment.
_request 里无论 HTTP 状态码/响应内容是什么都会直接 resp.json();当 Registry 返回非 JSON(例如 502 HTML、空响应等)时会抛出解码异常并掩盖原始错误信息。建议捕获 JSON 解码异常,在失败时至少用 resp.text/状态码组装错误,并确保仍抛出 GeneHubError。
| class GeneManifest(TypedDict, total=False): | ||
| slug: str | ||
| name: str | ||
| version: str | ||
| description: str | ||
| short_description: str | ||
| category: str | ||
| tags: list[str] | ||
| icon: str | None | ||
| author: Author | None | ||
| compatibility: list[CompatibilityEntry] | ||
| dependencies: list[DependencyEntry] | ||
| synergies: list[str] | ||
| skill: Skill | ||
| rules: list[Rule] | ||
| config: GeneConfig | None | ||
| learning: Learning | None | ||
|
|
There was a problem hiding this comment.
GeneManifest 定义为 TypedDict(total=False) 会让 slug/name/version/... 等在类型层面都变成可选,但实现里(例如 GenericAdapter.install)又直接用 manifest["slug"]/manifest["version"] 作为必填字段。建议把必填字段改为 Required(或将 TypedDict 设为 total=True 并对可选字段用 NotRequired/Optional),以免类型提示与实际约束不一致。
| "created_at": __import__("datetime") | ||
| .datetime.now(__import__("datetime").timezone.utc) | ||
| .isoformat(), |
There was a problem hiding this comment.
这里通过 __import__("datetime") 生成 created_at 可读性较差,也会让静态分析/重构工具更难处理。建议在模块顶部直接 from datetime import datetime, timezone(或 UTC)并使用常规调用来生成 ISO 时间戳。
|
|
||
| gene_yaml = target_dir / "gene.yaml" | ||
| gene_yaml.write_text( | ||
| yaml.dump(manifest, allow_unicode=True, default_flow_style=False), encoding="utf-8" |
There was a problem hiding this comment.
这里用 yaml.dump(...) 写 manifest,若传入的数据里混入非基础类型(自定义对象等)可能被序列化成 !!python/object 之类的 Python 专有标签,后续再用 safe_load 读取会失败并导致 list()/get_installed_version 跳过该基因。建议使用 yaml.safe_dump(并确保 manifest 仅包含 JSON/YAML 基础类型)来避免生成不可移植的 YAML。
| yaml.dump(manifest, allow_unicode=True, default_flow_style=False), encoding="utf-8" | |
| yaml.safe_dump(manifest, allow_unicode=True, default_flow_style=False), | |
| encoding="utf-8", |
…rningEngine)
Made-with: Cursor