store-api-py 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: store-api-py
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: FastAPI adapter that auto-generates RESTful APIs for py-store schemas (schema-driven CRUD over GQL)
5
5
  License: MIT
6
6
  Keywords: py-store,storepy,fastapi,rest,crud,gql
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "store-api-py"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "FastAPI adapter that auto-generates RESTful APIs for py-store schemas (schema-driven CRUD over GQL)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -11,7 +11,7 @@ import json
11
11
  from typing import Any, Callable
12
12
 
13
13
  from fastapi import FastAPI, Request
14
- from fastapi.responses import JSONResponse
14
+ from fastapi.responses import JSONResponse, Response
15
15
 
16
16
  from .errors import (
17
17
  StoreApiError,
@@ -49,6 +49,8 @@ def create_app(
49
49
  context_provider: ContextProvider | None = None,
50
50
  resources: list[str] | None = None,
51
51
  permission_error: type[BaseException] | None = None,
52
+ file_field: str = "file",
53
+ file_resolver: Callable[[Request, dict, str], Any] | None = None,
52
54
  ) -> FastAPI:
53
55
  """为 store(py-store 的 store 实例,需已 init + register)生成 RESTful FastAPI 应用。
54
56
 
@@ -97,7 +99,7 @@ def create_app(
97
99
  # 显式投影(spec/01+02):GQL 省略投影段 = 只返回 _id(三端 core 契约)。
98
100
  # 列表路由仅在 q 缺失时用它;q 存在时投影完全由 q 决定,适配层不追加。
99
101
  proj = _schema_projection(store, name)
100
- _register_resource(app, store, name, prefix, id_field, permission_error, proj)
102
+ _register_resource(app, store, name, prefix, id_field, permission_error, proj, file_field, file_resolver)
101
103
 
102
104
  return app
103
105
 
@@ -153,6 +155,8 @@ def _register_resource(
153
155
  id_field: str,
154
156
  permission_error: type[BaseException] | None,
155
157
  proj: str,
158
+ file_field: str,
159
+ file_resolver: Callable[[Request, dict, str], Any] | None,
156
160
  ) -> None:
157
161
  # 工厂函数隔离闭包:handler 签名只含 Request 与路径参数,
158
162
  # 否则 FastAPI 会把捕获变量解析成查询参数(缺参即 422)
@@ -174,6 +178,27 @@ def _register_resource(
174
178
  raise not_found(f"记录不存在: {id_field}={rid}")
175
179
  return {"data": data}
176
180
 
181
+ @app.get(base + "/{rid}/file")
182
+ @_guard(permission_error)
183
+ async def get_file(rid: str, request: Request):
184
+ rec = await store.query_one(f"{name}($condition: @c0){proj}", {"c0": {id_field: rid}})
185
+ if rec is None:
186
+ raise not_found(f"记录不存在: {id_field}={rid}")
187
+ raw = rec.get(file_field)
188
+ if raw is None:
189
+ raise not_found(f"文件不存在: {file_field}={rid}")
190
+ if file_resolver is not None:
191
+ out = file_resolver(request, rec, rid)
192
+ if inspect.isawaitable(out):
193
+ out = await out
194
+ else:
195
+ out = {"body": str(raw), "contentType": "text/plain; charset=utf-8", "fileName": f"{name}-{rid}"}
196
+ return Response(
197
+ content=out["body"],
198
+ media_type=out.get("contentType", "application/octet-stream"),
199
+ headers={"content-disposition": f'attachment; filename="{out.get("fileName", "file")}"'},
200
+ )
201
+
177
202
  @app.post(base, status_code=201)
178
203
  @_guard(permission_error)
179
204
  async def create_resource(request: Request):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: store-api-py
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: FastAPI adapter that auto-generates RESTful APIs for py-store schemas (schema-driven CRUD over GQL)
5
5
  License: MIT
6
6
  Keywords: py-store,storepy,fastapi,rest,crud,gql
@@ -223,3 +223,23 @@ def test_x_cache_header():
223
223
  store.cache_status = lambda: "HIT"
224
224
  hit = client.get("/api/user")
225
225
  assert hit.headers["x-cache"] == "HIT"
226
+
227
+
228
+ def test_file_download_route():
229
+ """文件下载路由:200 + 头;记录/字段缺失 → 404(spec/01+03+05)"""
230
+ store = MockStore()
231
+ client = TestClient(build_app(store))
232
+
233
+ created = client.post("/api/user", json={"name": "a", "age": 1})
234
+ rid = created.json()["data"]["_id"]
235
+ store.rows[rid]["file"] = "hello"
236
+
237
+ file = client.get(f"/api/user/{rid}/file")
238
+ assert file.status_code == 200
239
+ assert file.headers["content-type"] == "text/plain; charset=utf-8"
240
+ assert file.headers["content-disposition"] == f'attachment; filename="user-{rid}"'
241
+ assert file.content == b"hello"
242
+
243
+ missing = client.get("/api/user/nope/file")
244
+ assert missing.status_code == 404
245
+ assert missing.json()["error"]["code"] == "NOT_FOUND"
File without changes
File without changes