chore: 搭建项目骨架
- pyproject:hatchling + src 布局,核心依赖 psycopg/pydantic/pyyaml/cel-python - FastAPI 归入 [api] 可选依赖,开发依赖走 PEP 735 dependency-groups - 建立分层目录:ontology/compiler/storage/repository/graph/timeseries/expr/rules/api - errors.py 统一异常基类;py.typed 标记类型完整 - ruff(100) + pytest 配置;冒烟测试校验核心包不引入 fastapi/uvicorn
This commit is contained in:
1 parent
ecd5925b2c
commit
ace505cc6b
16 files changed
+1544
-11
No files matched your search
@@ -177,7 +177,7 @@ constraints:
|
|||||||
```python
|
```python
|
||||||
repo.save(Device(id="D-0001", serial="SN-88", model="X200", meta={"vendor": "acme"}))
|
repo.save(Device(id="D-0001", serial="SN-88", model="X200", meta={"vendor": "acme"}))
|
||||||
repo.save_many(devices)
|
repo.save_many(devices)
|
||||||
repo.patch("D-0001", {"meta.vendor": "acme-new"}) # 部分更新
|
repo.patch("D-0001", {"meta.vendor": "acme-new"}) # 部分更新
|
||||||
```
|
```
|
||||||
|
|
||||||
**时序写入**——数字孪生的遥测流,按时间追加:
|
**时序写入**——数字孪生的遥测流,按时间追加:
|
||||||
@@ -185,7 +185,7 @@ repo.patch("D-0001", {"meta.vendor": "acme-new"}) # 部分更新
|
|||||||
```python
|
```python
|
||||||
ts = adapter.timeseries("SensorReading")
|
ts = adapter.timeseries("SensorReading")
|
||||||
ts.write([{"ts": now, "metric": "temp", "value": 36.5, "tags": {"device": "D-0001"}}])
|
ts.write([{"ts": now, "metric": "temp", "value": 36.5, "tags": {"device": "D-0001"}}])
|
||||||
ts.write_batch(stream, batch_size=1000) # 批量/流式
|
ts.write_batch(stream, batch_size=1000) # 批量/流式
|
||||||
```
|
```
|
||||||
|
|
||||||
写入语义约定:`save` 按 `id` 幂等 upsert;`link()` 幂等建边;`constraints` 在提交前校验,失败抛 `ConstraintViolation`;批量写入单事务,超限自动分块。
|
写入语义约定:`save` 按 `id` 幂等 upsert;`link()` 幂等建边;`constraints` 在提交前校验,失败抛 `ConstraintViolation`;批量写入单事务,超限自动分块。
|
||||||
@@ -229,18 +229,25 @@ from adapter_python_pg import Adapter, ConstraintViolation
|
|||||||
|
|
||||||
adapter = Adapter.load("ontology/factory-twin.yaml", dsn="postgresql://...")
|
adapter = Adapter.load("ontology/factory-twin.yaml", dsn="postgresql://...")
|
||||||
|
|
||||||
repo = adapter.repository("Device") # 实体仓储
|
repo = adapter.repository("Device") # 实体仓储
|
||||||
repo.save(obj); repo.get(id); repo.find(**where); repo.patch(id, patch); repo.delete(id)
|
repo.save(obj)
|
||||||
|
repo.get(id)
|
||||||
|
repo.find(**where)
|
||||||
|
repo.patch(id, patch)
|
||||||
|
repo.delete(id)
|
||||||
|
|
||||||
graph = adapter.graph() # 图操作
|
graph = adapter.graph() # 图操作
|
||||||
graph.link(src, "mounted_on", dst, since=...); graph.traverse(...); graph.path(...)
|
graph.link(src, "mounted_on", dst, since=...)
|
||||||
|
graph.traverse(...)
|
||||||
|
graph.path(...)
|
||||||
|
|
||||||
ts = adapter.timeseries("SensorReading") # 时序
|
ts = adapter.timeseries("SensorReading") # 时序
|
||||||
ts.write(rows); ts.query(bucket="5m", agg="avg", **filters)
|
ts.write(rows)
|
||||||
|
ts.query(bucket="5m", agg="avg", **filters)
|
||||||
|
|
||||||
adapter.invoke("startLine", on="L-01", input={"deviceIds": ["D-0001"]}) # Actions
|
adapter.invoke("startLine", on="L-01", input={"deviceIds": ["D-0001"]}) # Actions
|
||||||
adapter.functions("Line")["utilization"] # Functions
|
adapter.functions("Line")["utilization"] # Functions
|
||||||
adapter.validate("SensorReading", row) # Constraints
|
adapter.validate("SensorReading", row) # Constraints
|
||||||
```
|
```
|
||||||
|
|
||||||
FastAPI 侧不提供独立服务:`build_router()` 返回一个 `APIRouter`,由宿主项目的 `app.include_router()` 挂进去。路由从本体自动生成,同一份 YAML 不重复声明:
|
FastAPI 侧不提供独立服务:`build_router()` 返回一个 `APIRouter`,由宿主项目的 `app.include_router()` 挂进去。路由从本体自动生成,同一份 YAML 不重复声明:
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
[project]
|
||||||
|
name = "adapter-python-pg"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "基于 ontology 数据基座的 PostgreSQL 中间层适配器(FastAPI 项目以 pip 包引用)"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
license = { text = "Proprietary" }
|
||||||
|
dependencies = [
|
||||||
|
"psycopg[binary,pool]>=3.2",
|
||||||
|
"pydantic>=2.9",
|
||||||
|
"pydantic-settings>=2.6",
|
||||||
|
"pyyaml>=6.0.2",
|
||||||
|
"cel-python>=0.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
# FastAPI 只是可选依赖:核心包不 import 它,宿主项目才需要
|
||||||
|
api = ["fastapi>=0.115"]
|
||||||
|
|
||||||
|
# 开发依赖走 PEP 735 依赖组:uv sync 默认安装,不需要 --extra dev
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
"pytest-asyncio>=0.24",
|
||||||
|
"testcontainers[postgres]>=4.8",
|
||||||
|
"ruff>=0.8",
|
||||||
|
"mypy>=1.13",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/adapter_python_pg"]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
package = true
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py311"
|
||||||
|
src = ["src", "tests"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
||||||
|
# E501: 行宽交给 formatter
|
||||||
|
# RUF001/002/003: 中文文档与注释里的全角标点是刻意的,不视为歧义字符
|
||||||
|
ignore = ["E501", "RUF001", "RUF002", "RUF003"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["src"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
|
markers = [
|
||||||
|
"integration: 需要真实 PostgreSQL 的测试(testcontainers)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.11"
|
||||||
|
files = ["src"]
|
||||||
|
strict = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""adapter_python_pg — ontology 驱动的 PostgreSQL 中间层适配器。
|
||||||
|
|
||||||
|
核心包**不导入** FastAPI;路由能力在 `adapter_python_pg.api`,且需要安装 `[api]` extra。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__all__ = ["Adapter", "__version__", "build_router"]
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover - 仅类型检查期可见
|
||||||
|
from adapter_python_pg.api.router import build_router
|
||||||
|
from adapter_python_pg.core import Adapter
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> Any:
|
||||||
|
"""延迟导入:`import adapter_python_pg` 不应拉起 psycopg / fastapi 之外的任何子系统。"""
|
||||||
|
if name == "Adapter":
|
||||||
|
from adapter_python_pg.core import Adapter
|
||||||
|
|
||||||
|
return Adapter
|
||||||
|
if name == "build_router":
|
||||||
|
from adapter_python_pg.api.router import build_router
|
||||||
|
|
||||||
|
return build_router
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""FastAPI router 工厂(可选依赖,核心包不得导入本模块)。"""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""本体 → 存储映射:幂等 DDL、索引推导、版本记录。"""
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""统一错误模型。
|
||||||
|
|
||||||
|
约定:所有对外抛出的异常都继承 `AdapterError`,宿主只需 catch 一个基类;
|
||||||
|
`ConstraintViolation` / `OntologyError` 属于调用方可恢复的输入类错误。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterError(Exception):
|
||||||
|
"""本适配器所有异常的基类。"""
|
||||||
|
|
||||||
|
|
||||||
|
class OntologyError(AdapterError):
|
||||||
|
"""本体 YAML 不合法:缺字段、类型不符、引用悬空。"""
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintViolation(AdapterError):
|
||||||
|
"""写入违反本体里的 constraints。"""
|
||||||
|
|
||||||
|
|
||||||
|
class EntityNotFound(AdapterError):
|
||||||
|
"""按 id 取实体时不存在。"""
|
||||||
|
|
||||||
|
|
||||||
|
class QueryError(AdapterError):
|
||||||
|
"""查询构造或执行失败(SQL / Cypher)。"""
|
||||||
|
|
||||||
|
|
||||||
|
class UnsafeIdentifier(QueryError):
|
||||||
|
"""标识符未通过白名单校验——通常是注入尝试或本体里写了非法名字。"""
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration(AdapterError):
|
||||||
|
"""连接、扩展缺失、版本不匹配等环境问题。"""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""表达式双轨:SQL 下推编译 + CEL 求值。"""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Apache AGE 封装:建图、建边、遍历、路径;转义与白名单集中于此。"""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""本体的解析与校验:YAML → pydantic spec。"""
|
||||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
|||||||
|
"""实体读写、批量、部分更新、约束校验。"""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""派生链接(传递闭包)、派生属性、计算属性。"""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""连接池、事务边界、动态 SQL 构建(标识符管控)。"""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""hypertable 读写、COPY 批量、聚合查询。"""
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""骨架冒烟测试:包能导入,且核心包不拉起 web 依赖(AGENTS.md §5 硬规则)。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_imports() -> None:
|
||||||
|
import adapter_python_pg
|
||||||
|
|
||||||
|
assert adapter_python_pg.__version__ == "0.1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_import_does_not_pull_fastapi() -> None:
|
||||||
|
"""在干净的解释器里 import 核心包,sys.modules 不应出现 fastapi / uvicorn。"""
|
||||||
|
code = (
|
||||||
|
"import sys, adapter_python_pg;"
|
||||||
|
"assert 'fastapi' not in sys.modules, 'core imported fastapi';"
|
||||||
|
"assert 'uvicorn' not in sys.modules, 'core imported uvicorn';"
|
||||||
|
"print('ok')"
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-c", code],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stdout.strip() == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_attribute_raises() -> None:
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import adapter_python_pg
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
_ = adapter_python_pg.nonexistent_attribute
|
||||||
Reference in new issue
Block a user