From 33eb5d54f199c56e5055bb6854416d5ae0c89b14 Mon Sep 17 00:00:00 2001 From: Donny Date: Thu, 5 Nov 2026 01:58:19 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=8A=80=E6=9C=AF=E6=A0=88=E4=B8=8E?= =?UTF-8?q?=E5=B7=A5=E7=A8=8B=E7=BA=A6=E6=9D=9F=E5=86=99=E5=85=A5=20AGENTS?= =?UTF-8?q?.md=EF=BC=8C=E4=BA=A4=E4=BB=98=E5=BD=A2=E6=80=81=E5=AE=9A?= =?UTF-8?q?=E4=B8=BA=20pip=20=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AGENTS.md:技术栈选型、五条架构决策、硬规则、目录、命令、测试策略 - 明确不提供独立 HTTP 服务:核心包禁止 fastapi,路由由宿主 include_router 挂载 - README 同步:集成方式改为纯库/挂路由/嵌入式三种,FastAPI 列为 [api] 可选依赖 - 非目标补充“不做独立服务” --- AGENTS.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 52 +++++++++++++++++------- 2 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..34753bf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,116 @@ +# adapter_python_pg 项目指令 + +基于 ontology(本体)数据基座的 **Python 中间层适配器**。用 YAML 定义实体、关系、属性、动作、规则,由本适配器驱动 PostgreSQL 完成读写与图查询,对上暴露实体/对象级的语义接口。 + +**这是一份给 AI 编码工具(Claude Code / Codex / Hermes / OpenCode)的项目约定**,优先级高于 README 的描述性内容;与本文冲突的实现一律视为错误。 + +--- + +## 1. 使用形态(硬规则) + +| 项 | 约定 | +|---|---| +| 交付物 | **pip 包**,被已有的 FastAPI 项目 `include_router` 引用 | +| 不做的事 | **不提供独立 HTTP 服务**,不带 `uvicorn` 启动入口,不假设自己是独立进程 | +| FastAPI 位置 | 只出现在 `adapter_python_pg/api/`,是 **optional extra**;核心包禁止 import fastapi | +| 宿主感知边界 | 宿主看不到 SQL / Cypher,只拿到实体对象与语义方法 | +| 契约来源 | 本体 YAML 是唯一事实来源;**只加载 YAML 即可工作**,不做代码生成、不要求手写 DDL | + +宿主项目的典型用法: + +```python +from fastapi import FastAPI +from adapter_python_pg import Adapter, build_router + +adapter = Adapter.load("ontology/factory-twin.yaml", dsn=settings.pg_dsn) +app = FastAPI() +app.include_router(build_router(adapter), prefix="/ontology") +``` + +## 2. 技术栈 + +| 层 | 选型 | 约束 | +|---|---|---| +| 语言 | Python **3.11+** | 与 `pricing-service`、`data-mould` 对齐 | +| 包管理 | **uv**(`uv.lock` 入库) | 不用手工 venv + pip | +| 构建 | hatchling | `src/` 布局,带 `py.typed` | +| 驱动 | **psycopg 3**(async + `psycopg_pool`) | 不用 asyncpg、不用任何 ORM | +| 图 | **`age`**(AGE 官方 psycopg3 驱动) | `AgeLoader` 处理 agtype,不手写解析 | +| 对象 | **pydantic v2** + `create_model()` | 运行期按本体动态建类 | +| 本体校验 | pydantic 模型 + 导出 JSON Schema | 本体先自查,schema 供其他语言消费 | +| 表达式 | **SQL 下推** + **CEL**(`cel-python`) | 不自研表达式解析器,不 `eval` | +| 批量写 | `COPY` 协议 | 时序吞吐量级差异 | +| API | FastAPI `add_api_route()` 动态注册 | 泛化 OpenAPI,不做 codegen | +| 测试 | pytest + pytest-asyncio + testcontainers | 集成测试连真库 | +| 质量 | ruff(line-length 100, py311)+ mypy | 配置写在 `pyproject.toml` | + +依赖分层:`[project.dependencies]` 只放核心(psycopg / age / pydantic / pyyaml / cel-python);fastapi 放 `[project.optional-dependencies].api`。 + +## 3. 目录结构 + +```text +src/adapter_python_pg/ +├── ontology/ YAML 解析与校验(pydantic spec:entities/links/actions/functions/rules/constraints) +├── compiler/ 本体 → 存储映射:幂等 DDL、索引推导、版本记录 +├── storage/ psycopg 连接池、事务边界、动态 SQL 构建(标识符管控) +├── repository/ 实体读写、批量、部分更新、约束校验 +├── graph/ AGE 封装:建图、建边、遍历、路径 —— 转义与白名单集中在这里 +├── timeseries/ hypertable 读写、COPY 批量、聚合查询 +├── expr/ 表达式双轨:SQL 下推编译 + CEL 求值 +├── rules/ 派生链接(传递闭包)、派生属性、计算属性 +├── api/ FastAPI router 工厂(optional) +└── errors.py 统一错误模型 +``` + +## 4. 架构决策(已拍板,勿反复) + +1. **异步优先 + 同步门面**:内部全 async;对同步调用方提供 `SyncAdapter` 薄包装,不复制实现。 +2. **多本体并存**:文档落在 `docs.collection = "{ontology}.{entity}"`;每个本体一个 AGE graph;多本体之间不做跨本体 JOIN。 +3. **先全 JSONB**:属性一律存 JSONB,`column: true`(属性提升为真实列)在 spec 里**预留字段但不实现**,等性能有真实压力再开。 +4. **FastAPI 路由动态生成**:`entity` 作为路径参数,OpenAPI 是泛化 schema;**不做**逐实体 typed routes、不导出 OpenAPI 代码。 +5. **发布方式**:先支持 `pip install git+ssh://git@gitlab.cnsjzn.com/donnyzhang/adapter_python_pg.git`;GitLab PyPI registry 后续再接。 + +## 5. 硬规则(违反即回退) + +- **核心包禁止 `import fastapi` / `import uvicorn`**。跑 `python -c "import adapter_python_pg"` 不应拉起任何 web 依赖。 +- **Cypher 禁止字符串拼接值**。所有进入 `cypher()` 的标识符走白名单、值走转义函数,集中在 `graph/`,并配独立的注入用例矩阵。 +- **实体与图必须同事务**。一个实体既有 JSONB 行又有图节点,写入在一个事务内完成;禁止 repository 与 graph 各自 commit 造成半实体。 +- **禁止 `eval` / `exec` / `importlib` 执行 YAML 内容**。表达式只允许两条路:编译成 SQL 下推,或交给 CEL 求值。 +- **DDL 必须幂等**(`IF NOT EXISTS`);本体演进只做加法,破坏性变更不做自动迁移。 +- **默认不建索引**。只有 `index: true` / `unique: true` 才建;GIN 不是默认项。 +- **公共接口必须完整类型标注**;动态 pydantic 模型只出现在数据边界,不穿透内部函数签名。 +- **不用 `print`**,用标准库 `logging`(库不配置 handler,由宿主决定)。 +- **测试不得写真实业务库**;集成测试用 testcontainers 起的临时实例。 + +## 6. 常用命令 + +```bash +uv sync --extra api # 安装(含 FastAPI 可选依赖) +uv sync # 只装核心,验证"无 web 依赖"边界 +uv run pytest # 全部测试 +uv run pytest -m "not integration" # 只跑不连库的单元测试 +uv run ruff check . && uv run ruff format --check . +uv run mypy src +``` + +## 7. 测试约定 + +| 层 | 做法 | +|---|---| +| 本体解析 / 编译 | 纯单元测试,断言生成的 SQL 字符串与索引清单 | +| 仓储 / 图 / 时序 | 集成测试,testcontainers 起 `pg-multimodel:pg18-age-timescale` | +| 注入防护 | 专门的用例矩阵:非法标识符、引号、`$$`、注释符、Unicode 绕过 | +| 回归 | 每个 bug 先写失败用例(RED),再修(GREEN) | + +集成测试镜像默认复用本机已构建的 `pg-multimodel:pg18-age-timescale`;CI 里改为拉 registry 里的预构建镜像,避免每次重新编译 AGE。 + +## 8. 与其他仓库的关系 + +| 仓库 | 关系 | +|---|---| +| `E:\code\ueos\devops\devos\server-resources\postgres-multimodel` | 底层多模型 PostgreSQL 的 Dockerfile / compose / 初始化脚本 | +| `ueos/backend` 的 `ontology` 模块(Java) | 概念同源(Action / Function / Query / 编排);**不共享代码**,接口语义对齐即可 | + +## 9. 当前状态 + +v0.1 设计阶段:README 与本文已定契约,代码未开始。落地顺序见 README「路线」:本体解析 → 文档读写 → 图 → 时序 → 推理 → FastAPI 路由 → Actions。 diff --git a/README.md b/README.md index c1b595a..4d71133 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ 面向其他项目的底层对接件,不是业务系统,也不是通用 ORM。上层项目只需要一份本体 YAML + 本适配器,就能对实体、关系、属性做对象级操作,而不用关心底下是文档表、图还是时序表。 > 状态:设计稿(v0.1)。仓库当前只有初始提交,实现尚未开始。本文是接口与存储映射的契约,先定契约再写代码。 +> +> 工程约定(技术栈、硬规则、目录、命令、测试策略)见 **`AGENTS.md`** —— 那是给编码工具和协作者看的执行约束,本文只讲"是什么、为什么"。 --- @@ -241,27 +243,48 @@ adapter.functions("Line")["utilization"] # Func adapter.validate("SensorReading", row) # Constraints ``` -FastAPI 侧自动从本体生成路由(同一份 YAML,不重复声明): +FastAPI 侧不提供独立服务:`build_router()` 返回一个 `APIRouter`,由宿主项目的 `app.include_router()` 挂进去。路由从本体自动生成,同一份 YAML 不重复声明: + +```python +from fastapi import FastAPI +from adapter_python_pg import Adapter, build_router + +adapter = Adapter.load("ontology/factory-twin.yaml", dsn=settings.pg_dsn) + +app = FastAPI() +app.include_router(build_router(adapter), prefix="/ontology") +``` ```text -GET /entities/{entity} 列表 / 过滤 -GET /entities/{entity}/{id} 单实体(可选 ?expand=links) -POST /entities/{entity} 写入 -PATCH /entities/{entity}/{id} 部分更新 -POST /graph/traverse 多跳查询 -POST /timeseries/{entity}/write 时序写入 -POST /timeseries/{entity}/query 时序聚合 -POST /actions/{action} 执行 Action -GET /ontology 返回已加载的本体元数据 +GET /ontology/entities/{entity} 列表 / 过滤 +GET /ontology/entities/{entity}/{id} 单实体(可选 ?expand=links) +POST /ontology/entities/{entity} 写入 +PATCH /ontology/entities/{entity}/{id} 部分更新 +POST /ontology/graph/traverse 多跳查询 +POST /ontology/timeseries/{entity}/write 时序写入 +POST /ontology/timeseries/{entity}/query 时序聚合 +POST /ontology/actions/{action} 执行 Action +GET /ontology/meta 返回已加载的本体元数据 ``` +OpenAPI 是**泛化 schema**(`entity` 走路径参数),不逐实体生成 typed 路由——这是刻意的取舍,见 AGENTS.md §4。 + ## 8. 集成到其他项目 -三种姿势,按侵入程度从低到高: +**交付形态是 pip 包,不是独立服务。** 本适配器不启动 HTTP 服务、不占端口、没有自己的 procfile——它住在宿主进程里。 -1. **HTTP**:独立跑 `uvicorn adapter_python_pg.app:app`,其他语言/服务直接调 REST。 -2. **Python 库**:`pip install -e .`,`Adapter.load()` 后当普通对象用,进程内零网络开销。 -3. **嵌入式**:把本体 YAML 打进宿主项目,adapter 作为其内部数据层,宿主不感知 PostgreSQL。 +```bash +pip install git+ssh://git@gitlab.cnsjzn.com/donnyzhang/adapter_python_pg.git +pip install "adapter_python_pg[api] @ git+ssh://..." # 需要 FastAPI 路由时 +``` + +三种用法,同一个包: + +1. **纯库(最常见)**:宿主只调 `Adapter` 的语义接口,不挂 HTTP 路由,FastAPI 依赖都不用装。 +2. **挂路由**:宿主已有 FastAPI 应用,`app.include_router(build_router(adapter))` 把它当子路由,权限、中间件、鉴权沿用宿主那套。 +3. **嵌入式**:本体 YAML 打进宿主仓库,adapter 作为宿主内部的数据层,外部完全不知道 PostgreSQL 存在。 + +核心依赖只有 psycopg / age / pydantic / pyyaml / cel-python;**FastAPI 属于 `[api]` 可选依赖**,装了才有路由能力。 ## 9. 与存储的关系 @@ -271,6 +294,7 @@ GET /ontology 返回已加载的本体元数据 ## 10. 非目标 +- **不做独立服务**:不提供可独立部署的 HTTP 服务、不做多租户网关、不接管鉴权(那是宿主的事) - 不做通用 ORM(不复刻 SQLAlchemy 的表达式系统;复杂 SQL 走 `raw()` 逃生口) - 不做本体编辑器 / 可视化(那是前端的事) - 不做数据迁移与版本演进工具(本体版本变更先手工处理,后续再谈)