KairoCore 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
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.
Potentially problematic release.
This version of KairoCore might be problematic. Click here for more details.
- KairoCore/__init__.py +9 -2
- KairoCore/common/errors.py +39 -1
- KairoCore/docs/CodeGenerateDoc.md +58 -0
- KairoCore/docs/FileUploadDoc.md +142 -0
- KairoCore/docs/HttpSessionDoc.md +170 -0
- KairoCore/docs/TokenUseDoc.md +349 -0
- KairoCore/docs/UseDoc.md +174 -0
- KairoCore/example/your_project_name/action/api_key_admin.py +42 -0
- KairoCore/example/your_project_name/action/auth.py +105 -0
- KairoCore/example/your_project_name/action/file_upload.py +71 -0
- KairoCore/example/your_project_name/action/http_demo.py +64 -0
- KairoCore/example/your_project_name/action/protected_demo.py +85 -0
- KairoCore/example/your_project_name/schema/auth.py +14 -0
- KairoCore/extensions/baidu/yijian.py +0 -0
- KairoCore/utils/auth.py +629 -0
- KairoCore/utils/kc_http.py +260 -0
- KairoCore/utils/kc_upload.py +218 -0
- KairoCore/utils/panic.py +21 -1
- KairoCore/utils/router.py +2 -1
- {kairocore-1.0.0.dist-info → kairocore-1.2.0.dist-info}/METADATA +5 -1
- {kairocore-1.0.0.dist-info → kairocore-1.2.0.dist-info}/RECORD +23 -8
- {kairocore-1.0.0.dist-info → kairocore-1.2.0.dist-info}/WHEEL +0 -0
- {kairocore-1.0.0.dist-info → kairocore-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件上传接口示例:提供两种上传方式
|
|
3
|
+
- multipart 上传(UploadFile):POST /upload
|
|
4
|
+
- Base64 上传(JSON):POST /upload_base64
|
|
5
|
+
|
|
6
|
+
签名约束:仅使用 query/body/file 三类参数名,符合 utils/router.enforce_signature 的规则。
|
|
7
|
+
返回:统一使用 kQuery.to_response 结构化响应。
|
|
8
|
+
"""
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from KairoCore import kcRouter, kQuery, Panic, KcUploader, exec_with_route_error
|
|
11
|
+
from KairoCore.common.errors import KCFU_UPLOAD_FAIL_ERROR, KCFU_BASE64_UPLOAD_FAIL_ERROR
|
|
12
|
+
from fastapi import UploadFile
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
router = kcRouter(tags=["文件上传"])
|
|
16
|
+
|
|
17
|
+
# multipart 上传:文件 + 可选目标目录
|
|
18
|
+
class UploadQuery(BaseModel):
|
|
19
|
+
target_dir: Optional[str] = Field(default="/tmp", description="文件保存目录")
|
|
20
|
+
filename: Optional[str] = Field(default=None, description="保存的文件名")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.post("/upload")
|
|
24
|
+
async def upload_file(query: UploadQuery, file: UploadFile):
|
|
25
|
+
"""
|
|
26
|
+
使用 KcUploader 保存 multipart/form-data 上传文件。
|
|
27
|
+
"""
|
|
28
|
+
uploader = KcUploader(default_target_dir="/tmp")
|
|
29
|
+
result = await exec_with_route_error(
|
|
30
|
+
uploader.save_upload_file(file=file, target_dir=query.target_dir, filename=query.filename),
|
|
31
|
+
KCFU_UPLOAD_FAIL_ERROR,
|
|
32
|
+
)
|
|
33
|
+
return kQuery.to_response(data=result, msg="上传成功")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Base64 上传:通过 JSON 传递字符串
|
|
37
|
+
class Base64Body(BaseModel):
|
|
38
|
+
content_base64: str = Field(description="Base64 编码的文件内容")
|
|
39
|
+
filename: str = Field(description="保存的文件名")
|
|
40
|
+
target_dir: Optional[str] = Field(default="/tmp", description="保存目录")
|
|
41
|
+
|
|
42
|
+
@router.post("/upload_base64")
|
|
43
|
+
async def upload_base64(body: Base64Body):
|
|
44
|
+
"""
|
|
45
|
+
使用 KcUploader 保存 Base64 编码内容为文件。
|
|
46
|
+
"""
|
|
47
|
+
uploader = KcUploader(default_target_dir="/tmp")
|
|
48
|
+
result = await exec_with_route_error(
|
|
49
|
+
uploader.save_base64(content_base64=body.content_base64, filename=body.filename, target_dir=body.target_dir),
|
|
50
|
+
KCFU_BASE64_UPLOAD_FAIL_ERROR,
|
|
51
|
+
)
|
|
52
|
+
return kQuery.to_response(data=result, msg="上传成功")
|
|
53
|
+
|
|
54
|
+
class DownloadQuery(BaseModel):
|
|
55
|
+
path: str = Field(description="服务器本地已保存的文件路径")
|
|
56
|
+
name: Optional[str] = Field(default=None, description="下载时展示的文件名(可选)")
|
|
57
|
+
inline: Optional[bool] = Field(default=False, description="是否内联显示(默认作为附件下载)")
|
|
58
|
+
|
|
59
|
+
@router.get("/download")
|
|
60
|
+
async def download_file(query: DownloadQuery):
|
|
61
|
+
"""
|
|
62
|
+
通过接口将服务器本地文件下载到用户电脑。
|
|
63
|
+
返回 FileResponse,浏览器将触发下载。
|
|
64
|
+
"""
|
|
65
|
+
uploader = KcUploader(default_target_dir="/tmp")
|
|
66
|
+
# 构建下载响应(支持自定义下载文件名、是否内联显示)
|
|
67
|
+
return await uploader.build_download_response(
|
|
68
|
+
src_path=query.path,
|
|
69
|
+
download_name=query.name,
|
|
70
|
+
inline=query.inline,
|
|
71
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP 会话示例路由:演示如何在项目中使用 KcHttpSession 进行异步 HTTP 请求
|
|
3
|
+
- GET 示例:请求 httpbin.org/get 并回显查询参数
|
|
4
|
+
- POST 示例:请求 httpbin.org/post 并回显提交的 JSON
|
|
5
|
+
- 状态码示例:请求 httpbin.org/status/{code} 展示错误处理(4xx/5xx)
|
|
6
|
+
- 下载示例:下载 httpbin 的图片到本地 /tmp 目录
|
|
7
|
+
|
|
8
|
+
说明:
|
|
9
|
+
- 仅使用框架允许的参数名 query/body,保持与现有签名强制规则一致
|
|
10
|
+
- 使用 async with 上下文管理自动释放连接资源
|
|
11
|
+
"""
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from KairoCore import kcRouter, kQuery, KcHttpSession
|
|
16
|
+
|
|
17
|
+
# 分组到文档中 "HTTP会话示例"
|
|
18
|
+
router = kcRouter(tags=["HTTP会话示例"])
|
|
19
|
+
|
|
20
|
+
class GetDemoQuery(BaseModel):
|
|
21
|
+
foo: Optional[str] = "bar"
|
|
22
|
+
|
|
23
|
+
class PostDemoBody(BaseModel):
|
|
24
|
+
x: int = 123
|
|
25
|
+
msg: str = "hello"
|
|
26
|
+
|
|
27
|
+
class StatusQuery(BaseModel):
|
|
28
|
+
code: int = 200
|
|
29
|
+
|
|
30
|
+
class DownloadQuery(BaseModel):
|
|
31
|
+
# 需要下载的 URL(示例默认是 httpbin 的 png 图片)
|
|
32
|
+
url: str = "https://httpbin.org/image/png"
|
|
33
|
+
# 保存路径(默认保存到 /tmp/httpbin.png)
|
|
34
|
+
save_path: str = "/tmp/httpbin.png"
|
|
35
|
+
|
|
36
|
+
@router.get("/get")
|
|
37
|
+
async def demo_get(query: GetDemoQuery):
|
|
38
|
+
"""演示 GET 请求:将 query.foo 作为查询参数发送到 httpbin,并返回响应"""
|
|
39
|
+
async with KcHttpSession(base_url="https://httpbin.org", timeout=10, retries=2) as http:
|
|
40
|
+
resp = await http.get("/get", params={"foo": query.foo})
|
|
41
|
+
# 统一响应封装,直接返回 httpbin 的响应数据
|
|
42
|
+
return kQuery.to_response(data=resp.data, msg="GET 成功")
|
|
43
|
+
|
|
44
|
+
@router.post("/post")
|
|
45
|
+
async def demo_post(body: PostDemoBody):
|
|
46
|
+
"""演示 POST 请求:将 body 序列化为 JSON 发送到 httpbin,并返回响应"""
|
|
47
|
+
async with KcHttpSession(base_url="https://httpbin.org", timeout=10, retries=2) as http:
|
|
48
|
+
resp = await http.post("/post", json=body.model_dump())
|
|
49
|
+
return kQuery.to_response(data=resp.data, msg="POST 成功")
|
|
50
|
+
|
|
51
|
+
@router.get("/status")
|
|
52
|
+
async def demo_status(query: StatusQuery):
|
|
53
|
+
"""演示状态码处理:请求 /status/{code},当 code 为 4xx/5xx 时会抛出统一异常并被全局捕获"""
|
|
54
|
+
async with KcHttpSession(base_url="https://httpbin.org", timeout=5, retries=1) as http:
|
|
55
|
+
# 2xx 时正常返回;4xx/5xx 时将抛出 Panic(KCHT_STATUS_ERROR/KCHT_REQUEST_ERROR),由全局异常处理器接管
|
|
56
|
+
resp = await http.get(f"/status/{query.code}")
|
|
57
|
+
return kQuery.to_response(data={"status_code": resp.status_code}, msg="状态码请求成功")
|
|
58
|
+
|
|
59
|
+
@router.get("/download")
|
|
60
|
+
async def demo_download(query: DownloadQuery):
|
|
61
|
+
"""演示文件下载:下载指定 URL 到本地 save_path"""
|
|
62
|
+
async with KcHttpSession(timeout=10, retries=1) as http:
|
|
63
|
+
path = await http.download(url=query.url, save_path=query.save_path)
|
|
64
|
+
return kQuery.to_response(data={"saved": path}, msg="下载完成")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
受保护接口示例:演示普通接口如何带上 token 校验进行请求
|
|
3
|
+
|
|
4
|
+
- 基础校验:只要求 access token(/ping)
|
|
5
|
+
- 多租户校验:要求 access token + 租户 tid(/tenant/ping)
|
|
6
|
+
- 角色校验:要求 access token + 具备 admin 角色(/admin/ping)
|
|
7
|
+
- 仅限 API_KEY:无需登录,持有永久有效 API_KEY 即可(/api-key/ping)
|
|
8
|
+
|
|
9
|
+
调用方式:
|
|
10
|
+
- 使用 Authorization 头:Authorization: Bearer <access_token>
|
|
11
|
+
- 或在浏览器场景使用 HttpOnly Cookie:access_token=<access_token>
|
|
12
|
+
- 使用 X-API-Key 头或查询参数 api_key=<key>
|
|
13
|
+
|
|
14
|
+
完整路径前缀(默认):/example/api/protected_demo/*
|
|
15
|
+
"""
|
|
16
|
+
from typing import Any, Dict
|
|
17
|
+
from fastapi import APIRouter, Depends
|
|
18
|
+
|
|
19
|
+
from KairoCore import kcRouter, kQuery, KairoAuth
|
|
20
|
+
|
|
21
|
+
# 文档分组到 "Token校验示例"
|
|
22
|
+
router = kcRouter(tags=["Token校验示例"])
|
|
23
|
+
|
|
24
|
+
# 1) 仅要求 access token
|
|
25
|
+
base_router = APIRouter(dependencies=[Depends(KairoAuth.require_access_token)])
|
|
26
|
+
|
|
27
|
+
@base_router.get("/ping")
|
|
28
|
+
async def ping() -> Dict[str, Any]:
|
|
29
|
+
principal = KairoAuth.get_current_principal() or {}
|
|
30
|
+
return kQuery.to_response(
|
|
31
|
+
data={
|
|
32
|
+
"message": "pong",
|
|
33
|
+
"user_id": principal.get("sub"),
|
|
34
|
+
"roles": principal.get("roles") or [],
|
|
35
|
+
},
|
|
36
|
+
msg="ok",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# 2) 要求存在租户 tid
|
|
40
|
+
tenant_router = APIRouter(dependencies=[Depends(KairoAuth.require_access_token), Depends(KairoAuth.require_tenant)])
|
|
41
|
+
|
|
42
|
+
@tenant_router.get("/tenant/ping")
|
|
43
|
+
async def tenant_ping() -> Dict[str, Any]:
|
|
44
|
+
principal = KairoAuth.get_current_principal() or {}
|
|
45
|
+
return kQuery.to_response(
|
|
46
|
+
data={
|
|
47
|
+
"message": "pong-tenant",
|
|
48
|
+
"tenant_id": principal.get("tid"),
|
|
49
|
+
},
|
|
50
|
+
msg="ok",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 3) 要求具有 admin 角色
|
|
54
|
+
admin_router = APIRouter(dependencies=[Depends(KairoAuth.require_access_token), Depends(KairoAuth.require_roles(["admin"]))])
|
|
55
|
+
|
|
56
|
+
@admin_router.get("/admin/ping")
|
|
57
|
+
async def admin_ping() -> Dict[str, Any]:
|
|
58
|
+
principal = KairoAuth.get_current_principal() or {}
|
|
59
|
+
return kQuery.to_response(
|
|
60
|
+
data={
|
|
61
|
+
"message": "pong-admin",
|
|
62
|
+
"roles": principal.get("roles") or [],
|
|
63
|
+
},
|
|
64
|
+
msg="ok",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 4) 仅限 API_KEY,无需 access token
|
|
68
|
+
api_key_router = APIRouter(dependencies=[Depends(KairoAuth.require_api_key)])
|
|
69
|
+
|
|
70
|
+
@api_key_router.get("/api-key/ping")
|
|
71
|
+
async def api_key_ping() -> Dict[str, Any]:
|
|
72
|
+
principal = KairoAuth.get_current_principal() or {}
|
|
73
|
+
return kQuery.to_response(
|
|
74
|
+
data={
|
|
75
|
+
"message": "pong-api-key",
|
|
76
|
+
"principal": principal, # 对于 API_KEY 认证,principal 可能为 {"api_key": "...", "type": "api_key"}
|
|
77
|
+
},
|
|
78
|
+
msg="ok",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# 将子路由挂载到主路由(最终路径会是 /example/api/protected_demo/...)
|
|
82
|
+
router.include_router(base_router, prefix="")
|
|
83
|
+
router.include_router(tenant_router, prefix="")
|
|
84
|
+
router.include_router(admin_router, prefix="")
|
|
85
|
+
router.include_router(api_key_router, prefix="")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
class LoginBody(BaseModel):
|
|
5
|
+
username: str
|
|
6
|
+
password: str
|
|
7
|
+
tenant_id: Optional[str] = None
|
|
8
|
+
roles: Optional[List[str]] = None
|
|
9
|
+
|
|
10
|
+
class RefreshBody(BaseModel):
|
|
11
|
+
refresh_token: str
|
|
12
|
+
|
|
13
|
+
class LogoutBody(BaseModel):
|
|
14
|
+
refresh_token: str
|
|
File without changes
|