ys_base 0.1.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.
- ys_base/__init__.py +4 -0
- ys_base/auth/__init__.py +5 -0
- ys_base/auth/client.py +151 -0
- ys_base/auth/delta.py +95 -0
- ys_base/contracts/__init__.py +123 -0
- ys_base/contracts/base.py +77 -0
- ys_base/contracts/enums.py +201 -0
- ys_base/contracts/master.py +245 -0
- ys_base/contracts/platform.py +98 -0
- ys_base/contracts/relations.py +104 -0
- ys_base/py.typed +0 -0
- ys_base/server/__init__.py +13 -0
- ys_base/server/app_server.py +339 -0
- ys_base/server/jwt_verify.py +58 -0
- ys_base/server/router.py +35 -0
- ys_base/store/__init__.py +5 -0
- ys_base/store/base.py +49 -0
- ys_base/store/sqlite.py +146 -0
- ys_base/trade/__init__.py +33 -0
- ys_base/trade/adapter.py +54 -0
- ys_base/trade/client_base.py +988 -0
- ys_base/trade/client_impl.py +535 -0
- ys_base/trade/profit.py +452 -0
- ys_base/trade/quote.py +227 -0
- ys_base/trade/reconciler.py +124 -0
- ys_base/trade/settings.py +55 -0
- ys_base/ws/__init__.py +9 -0
- ys_base/ws/rpc_client.py +248 -0
- ys_base-0.1.0.dist-info/METADATA +37 -0
- ys_base-0.1.0.dist-info/RECORD +31 -0
- ys_base-0.1.0.dist-info/WHEEL +4 -0
ys_base/__init__.py
ADDED
ys_base/auth/__init__.py
ADDED
ys_base/auth/client.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""AuthClient v2 —— 子系统连接 auth 平台的客户端。
|
|
3
|
+
|
|
4
|
+
流程(架构方案 v5 §1 / v6 §1):
|
|
5
|
+
hello → register_app(HMAC 挑战应答) → sync(游标上报) → snapshot/delta 持续接收
|
|
6
|
+
控制事件:user_revoked(踢用户)、app_status(可插拔状态机)、resync(授权变更重拉)。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import hmac
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Awaitable, Callable
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
from ys_base.auth.delta import DeltaSync
|
|
19
|
+
from ys_base.ws.rpc_client import WsRpcClient, YsRejected
|
|
20
|
+
|
|
21
|
+
RevokedCallback = Callable[[str, str], Awaitable[None]] # (user_id, reason)
|
|
22
|
+
StatusCallback = Callable[[str], Awaitable[None]] # (app_status)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class AppCredentials:
|
|
27
|
+
app_name: str
|
|
28
|
+
secret: str
|
|
29
|
+
|
|
30
|
+
def sign(self, connection_id: str, ts: float) -> str:
|
|
31
|
+
message = f"{connection_id}:{ts}".encode()
|
|
32
|
+
return hmac.new(self.secret.encode(), message, hashlib.sha256).hexdigest()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AuthClient(WsRpcClient):
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
uri: str,
|
|
39
|
+
credentials: AppCredentials,
|
|
40
|
+
*,
|
|
41
|
+
delta_sync: DeltaSync | None = None,
|
|
42
|
+
app_version: str = "",
|
|
43
|
+
**kwargs: Any,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(uri, **kwargs)
|
|
46
|
+
self.credentials = credentials
|
|
47
|
+
self.delta = delta_sync
|
|
48
|
+
self.app_version = app_version
|
|
49
|
+
self.registered = False
|
|
50
|
+
self.app_id: str = ""
|
|
51
|
+
self.server_contracts_version: str = ""
|
|
52
|
+
self._revoked_callbacks: list[RevokedCallback] = []
|
|
53
|
+
self._status_callbacks: list[StatusCallback] = []
|
|
54
|
+
if self.delta is not None:
|
|
55
|
+
self.delta.bind_resync(self._request_resync)
|
|
56
|
+
|
|
57
|
+
# ---------- 回调注册 ----------
|
|
58
|
+
|
|
59
|
+
def on_revoked(self, callback: RevokedCallback) -> None:
|
|
60
|
+
self._revoked_callbacks.append(callback)
|
|
61
|
+
|
|
62
|
+
def on_app_status(self, callback: StatusCallback) -> None:
|
|
63
|
+
self._status_callbacks.append(callback)
|
|
64
|
+
|
|
65
|
+
# ---------- 握手 ----------
|
|
66
|
+
|
|
67
|
+
async def _handle_hello(self, data: dict[str, Any]) -> None:
|
|
68
|
+
self.registered = False
|
|
69
|
+
self.server_contracts_version = str(data.get("contracts_version", ""))
|
|
70
|
+
ts = time.time()
|
|
71
|
+
try:
|
|
72
|
+
result = await self.call(
|
|
73
|
+
"register_app",
|
|
74
|
+
{
|
|
75
|
+
"app_name": self.credentials.app_name,
|
|
76
|
+
"signature": self.credentials.sign(self.connection_id, ts),
|
|
77
|
+
"ts": ts,
|
|
78
|
+
"sdk_version": self._sdk_version(),
|
|
79
|
+
"app_version": self.app_version,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
except YsRejected as e:
|
|
83
|
+
logger.error("应用注册被拒: {}(状态可能为 disabled,按降级策略处理)", e)
|
|
84
|
+
for callback in self._status_callbacks:
|
|
85
|
+
await callback("disabled")
|
|
86
|
+
return
|
|
87
|
+
self.app_id = str((result or {}).get("app_id", ""))
|
|
88
|
+
self.registered = True
|
|
89
|
+
logger.info("应用注册成功: {} (app_id={})", self.credentials.app_name, self.app_id)
|
|
90
|
+
if self.delta is not None:
|
|
91
|
+
await self.send_event("sync", {"cursors": await self.delta.build_cursors()})
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _sdk_version() -> str:
|
|
95
|
+
from ys_base import __version__
|
|
96
|
+
|
|
97
|
+
return __version__
|
|
98
|
+
|
|
99
|
+
async def _request_resync(self, tables: list[str]) -> None:
|
|
100
|
+
cursors = {table: 0 for table in tables}
|
|
101
|
+
await self.send_event("sync", {"cursors": cursors})
|
|
102
|
+
|
|
103
|
+
# ---------- 事件分发 ----------
|
|
104
|
+
|
|
105
|
+
async def _dispatch_event(self, event: str, data: dict[str, Any]) -> None:
|
|
106
|
+
if event == "snapshot" and self.delta is not None:
|
|
107
|
+
await self.delta.handle_snapshot(data)
|
|
108
|
+
return
|
|
109
|
+
if event == "delta" and self.delta is not None:
|
|
110
|
+
await self.delta.handle_delta(data)
|
|
111
|
+
return
|
|
112
|
+
if event == "resync" and self.delta is not None:
|
|
113
|
+
await self.delta.handle_resync(data)
|
|
114
|
+
return
|
|
115
|
+
if event == "user_revoked":
|
|
116
|
+
user_id = str(data.get("user_id", ""))
|
|
117
|
+
reason = str(data.get("reason", ""))
|
|
118
|
+
for callback in self._revoked_callbacks:
|
|
119
|
+
try:
|
|
120
|
+
await callback(user_id, reason)
|
|
121
|
+
except Exception:
|
|
122
|
+
logger.exception("user_revoked 回调异常")
|
|
123
|
+
return
|
|
124
|
+
if event == "app_status":
|
|
125
|
+
status = str(data.get("status", ""))
|
|
126
|
+
for callback in self._status_callbacks:
|
|
127
|
+
try:
|
|
128
|
+
await callback(status)
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.exception("app_status 回调异常")
|
|
131
|
+
return
|
|
132
|
+
await super()._dispatch_event(event, data)
|
|
133
|
+
|
|
134
|
+
# ---------- 业务接口 ----------
|
|
135
|
+
|
|
136
|
+
async def login(self, info: dict[str, Any]) -> dict[str, Any]:
|
|
137
|
+
"""转发终端用户登录(返回用户信息 + token 对)。"""
|
|
138
|
+
return await self.call("login", info)
|
|
139
|
+
|
|
140
|
+
async def exchange_sso_code(self, code: str) -> dict[str, Any]:
|
|
141
|
+
"""门户一次性 code 换 token(SSO)。"""
|
|
142
|
+
return await self.call("exchange_sso_code", {"code": code})
|
|
143
|
+
|
|
144
|
+
async def heartbeat(self, checks: dict[str, str] | None = None, uptime: float = 0) -> None:
|
|
145
|
+
cursors = await self.delta.build_cursors() if self.delta is not None else {}
|
|
146
|
+
await self.send_event(
|
|
147
|
+
"heartbeat",
|
|
148
|
+
{"cursors": cursors, "uptime": uptime, "healthy": all(
|
|
149
|
+
v == "ok" for v in (checks or {}).values()
|
|
150
|
+
), "checks": checks or {}},
|
|
151
|
+
)
|
ys_base/auth/delta.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DeltaSync —— 差量同步引擎(客户端侧)。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
- 维护各表游标,生成 sync 上报;
|
|
6
|
+
- 应用 snapshot(分批聚合)与 delta(幂等);
|
|
7
|
+
- seq 空洞检测 → 请求重新同步该表;
|
|
8
|
+
- 表级回调通知业务层。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
from ys_base.store.base import LocalStore
|
|
17
|
+
|
|
18
|
+
TableCallback = Callable[[str, list[dict[str, Any]]], Awaitable[None]]
|
|
19
|
+
ResyncRequest = Callable[[list[str]], Awaitable[None]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeltaSync:
|
|
23
|
+
def __init__(self, store: LocalStore, tables: list[str]):
|
|
24
|
+
self.store = store
|
|
25
|
+
self.tables = list(tables)
|
|
26
|
+
self._snapshot_buffer: dict[str, list[dict[str, Any]]] = {}
|
|
27
|
+
self._callbacks: dict[str, list[TableCallback]] = {}
|
|
28
|
+
self._request_resync: ResyncRequest | None = None
|
|
29
|
+
|
|
30
|
+
def on_table(self, table: str, callback: TableCallback) -> None:
|
|
31
|
+
"""注册表变更回调(收到差量/快照落库后触发,参数为本批文档)。"""
|
|
32
|
+
self._callbacks.setdefault(table, []).append(callback)
|
|
33
|
+
|
|
34
|
+
def bind_resync(self, request_resync: ResyncRequest) -> None:
|
|
35
|
+
self._request_resync = request_resync
|
|
36
|
+
|
|
37
|
+
async def build_cursors(self) -> dict[str, int]:
|
|
38
|
+
return {table: await self.store.get_cursor(table) for table in self.tables}
|
|
39
|
+
|
|
40
|
+
# ---------- 事件入口(由 AuthClient 调用) ----------
|
|
41
|
+
|
|
42
|
+
async def handle_snapshot(self, data: dict[str, Any]) -> None:
|
|
43
|
+
table = data["table"]
|
|
44
|
+
batch, total = int(data.get("batch", 1)), int(data.get("total", 1))
|
|
45
|
+
docs = data.get("docs", [])
|
|
46
|
+
buffer = self._snapshot_buffer.setdefault(table, [])
|
|
47
|
+
buffer.extend(docs)
|
|
48
|
+
if batch < total:
|
|
49
|
+
return
|
|
50
|
+
collected = self._snapshot_buffer.pop(table, [])
|
|
51
|
+
last_seq = int(data.get("last_seq", 0))
|
|
52
|
+
await self.store.replace_table(table, collected, last_seq)
|
|
53
|
+
logger.info("快照落地: {} 共 {} 条 (seq={})", table, len(collected), last_seq)
|
|
54
|
+
await self._notify(table, collected)
|
|
55
|
+
|
|
56
|
+
async def handle_delta(self, data: dict[str, Any]) -> None:
|
|
57
|
+
table = data["table"]
|
|
58
|
+
changes: list[dict[str, Any]] = data.get("changes", [])
|
|
59
|
+
last_seq = int(data.get("last_seq", 0))
|
|
60
|
+
if not changes:
|
|
61
|
+
return
|
|
62
|
+
cursor = await self.store.get_cursor(table)
|
|
63
|
+
first_seq = int(changes[0].get("seq", 0))
|
|
64
|
+
if first_seq > cursor + 1:
|
|
65
|
+
logger.warning("检测到 seq 空洞: {} cursor={} first={},请求重新同步", table, cursor, first_seq)
|
|
66
|
+
await self._resync([table])
|
|
67
|
+
return
|
|
68
|
+
effective = [c for c in changes if int(c.get("seq", 0)) > cursor]
|
|
69
|
+
if not effective:
|
|
70
|
+
return
|
|
71
|
+
await self.store.apply_changes(table, effective, last_seq)
|
|
72
|
+
await self._notify(table, [c.get("doc", {}) for c in effective])
|
|
73
|
+
|
|
74
|
+
async def handle_resync(self, data: dict[str, Any]) -> None:
|
|
75
|
+
"""服务端要求重拉(授权变更等)。"""
|
|
76
|
+
tables = data.get("tables") or self.tables
|
|
77
|
+
for table in tables:
|
|
78
|
+
self._snapshot_buffer.pop(table, None)
|
|
79
|
+
await self.store.replace_table(table, [], 0)
|
|
80
|
+
await self._resync(list(tables))
|
|
81
|
+
|
|
82
|
+
# ---------- 内部 ----------
|
|
83
|
+
|
|
84
|
+
async def _resync(self, tables: list[str]) -> None:
|
|
85
|
+
if self._request_resync is None:
|
|
86
|
+
logger.error("未绑定 resync 请求通道,无法补差: {}", tables)
|
|
87
|
+
return
|
|
88
|
+
await self._request_resync(tables)
|
|
89
|
+
|
|
90
|
+
async def _notify(self, table: str, docs: list[dict[str, Any]]) -> None:
|
|
91
|
+
for callback in self._callbacks.get(table, []):
|
|
92
|
+
try:
|
|
93
|
+
await callback(table, docs)
|
|
94
|
+
except Exception:
|
|
95
|
+
logger.exception("表回调异常: {}", table)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ys_base.contracts —— 全平台跨系统契约(唯一事实来源)。
|
|
3
|
+
|
|
4
|
+
TABLES 注册表:表名 ↔ 模型 ↔ 订阅授权枚举,供 DeltaSync / 服务端下发管道 / codegen 使用。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from ys_base.contracts.base import (
|
|
10
|
+
SField,
|
|
11
|
+
YsModel,
|
|
12
|
+
field_sensitivity,
|
|
13
|
+
fields_for_grant,
|
|
14
|
+
project_doc,
|
|
15
|
+
)
|
|
16
|
+
from ys_base.contracts.enums import * # noqa: F401,F403
|
|
17
|
+
from ys_base.contracts.enums import AccessTable, export_enum_meta
|
|
18
|
+
from ys_base.contracts.master import (
|
|
19
|
+
Account,
|
|
20
|
+
BankCard,
|
|
21
|
+
Broker,
|
|
22
|
+
CounterSupplier,
|
|
23
|
+
Custodian,
|
|
24
|
+
ExchangeAccount,
|
|
25
|
+
MountExchangeAccount,
|
|
26
|
+
Owner,
|
|
27
|
+
Project,
|
|
28
|
+
Server,
|
|
29
|
+
ServerRef,
|
|
30
|
+
ServerRoom,
|
|
31
|
+
Strategy,
|
|
32
|
+
Team,
|
|
33
|
+
TradingPlatform,
|
|
34
|
+
User,
|
|
35
|
+
)
|
|
36
|
+
from ys_base.contracts.platform import (
|
|
37
|
+
App,
|
|
38
|
+
AppConfig,
|
|
39
|
+
ChangelogEntry,
|
|
40
|
+
FileMeta,
|
|
41
|
+
RefreshToken,
|
|
42
|
+
RowFilter,
|
|
43
|
+
)
|
|
44
|
+
from ys_base.contracts.relations import (
|
|
45
|
+
AccountApp,
|
|
46
|
+
AccountStrategy,
|
|
47
|
+
FOREIGN_KEYS,
|
|
48
|
+
ForeignKey,
|
|
49
|
+
OwnerApp,
|
|
50
|
+
OwnerStrategy,
|
|
51
|
+
ProjectOwner,
|
|
52
|
+
UserApp,
|
|
53
|
+
foreign_keys_to,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
#: 表名(=Mongo 集合名/SQLite 表名) → 模型
|
|
57
|
+
TABLES: dict[str, type[YsModel]] = {
|
|
58
|
+
"teams": Team,
|
|
59
|
+
"owners": Owner,
|
|
60
|
+
"accounts": Account,
|
|
61
|
+
"exchange_accounts": ExchangeAccount,
|
|
62
|
+
"mount_exchange_accounts": MountExchangeAccount,
|
|
63
|
+
"users": User,
|
|
64
|
+
"bank_cards": BankCard,
|
|
65
|
+
"custodians": Custodian,
|
|
66
|
+
"strategies": Strategy,
|
|
67
|
+
"projects": Project,
|
|
68
|
+
"brokers": Broker,
|
|
69
|
+
"servers": Server,
|
|
70
|
+
"trading_platforms": TradingPlatform,
|
|
71
|
+
"counter_suppliers": CounterSupplier,
|
|
72
|
+
"server_rooms": ServerRoom,
|
|
73
|
+
"user_app": UserApp,
|
|
74
|
+
"account_app": AccountApp,
|
|
75
|
+
"owner_app": OwnerApp,
|
|
76
|
+
"account_strategy": AccountStrategy,
|
|
77
|
+
"owner_strategy": OwnerStrategy,
|
|
78
|
+
"project_owner": ProjectOwner,
|
|
79
|
+
"apps": App,
|
|
80
|
+
"app_configs": AppConfig,
|
|
81
|
+
"file_metas": FileMeta,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#: 订阅授权枚举 → 该授权可订阅的表集合(闭包:授权 OWNER 即含其关联台账等)
|
|
85
|
+
ACCESS_TABLES: dict[AccessTable, tuple[str, ...]] = {
|
|
86
|
+
AccessTable.USER: ("users", "user_app"),
|
|
87
|
+
AccessTable.TEAM: ("teams",),
|
|
88
|
+
AccessTable.OWNER: (
|
|
89
|
+
"owners",
|
|
90
|
+
"owner_app",
|
|
91
|
+
"owner_strategy",
|
|
92
|
+
"exchange_accounts",
|
|
93
|
+
"bank_cards",
|
|
94
|
+
),
|
|
95
|
+
AccessTable.ACCOUNT: (
|
|
96
|
+
"accounts",
|
|
97
|
+
"account_app",
|
|
98
|
+
"account_strategy",
|
|
99
|
+
"mount_exchange_accounts",
|
|
100
|
+
"brokers",
|
|
101
|
+
"servers",
|
|
102
|
+
),
|
|
103
|
+
AccessTable.CUSTODIAN: ("custodians",),
|
|
104
|
+
AccessTable.PROJECT: ("projects", "project_owner", "strategies"),
|
|
105
|
+
AccessTable.APP_CONFIG: ("app_configs",),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
__all__ = [ # noqa: RUF022
|
|
109
|
+
"YsModel", "SField", "project_doc", "fields_for_grant", "field_sensitivity",
|
|
110
|
+
"export_enum_meta", "TABLES", "ACCESS_TABLES", "ForeignKey", "FOREIGN_KEYS",
|
|
111
|
+
"foreign_keys_to",
|
|
112
|
+
"Team", "Owner", "Account", "ExchangeAccount", "MountExchangeAccount", "User",
|
|
113
|
+
"BankCard", "Custodian", "Strategy", "Project", "Broker", "Server", "ServerRef",
|
|
114
|
+
"TradingPlatform", "CounterSupplier", "ServerRoom",
|
|
115
|
+
"UserApp", "AccountApp", "OwnerApp", "AccountStrategy", "OwnerStrategy", "ProjectOwner",
|
|
116
|
+
"App", "AppConfig", "RowFilter", "ChangelogEntry", "RefreshToken", "FileMeta",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def model_for_table(table: str) -> type[BaseModel]:
|
|
121
|
+
if table not in TABLES:
|
|
122
|
+
raise KeyError(f"未注册的契约表: {table}")
|
|
123
|
+
return TABLES[table]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""契约基类与字段级投影工具。
|
|
3
|
+
|
|
4
|
+
约定(架构方案 v6):
|
|
5
|
+
- 主键 `_id` = ObjectId hex 字符串,模型内属性名 `id`;
|
|
6
|
+
- created_at/updated_at/deleted_at 为 epoch 秒;deleted_at 非空即软删除;
|
|
7
|
+
- 字段敏感级经 Field 的 json_schema_extra["sensitivity"] 标注,
|
|
8
|
+
下发管道按 App 的 FieldGrant 用 project_doc() 投影裁剪(剔除字段而非置空)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
|
+
|
|
16
|
+
from ys_base.contracts.enums import GRANT_ALLOWS, FieldGrant, Sensitivity
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def SField(
|
|
20
|
+
default: Any = ...,
|
|
21
|
+
*,
|
|
22
|
+
sensitivity: Sensitivity,
|
|
23
|
+
title: str | None = None,
|
|
24
|
+
**kwargs: Any,
|
|
25
|
+
) -> Any:
|
|
26
|
+
"""带敏感级标注的 Field。"""
|
|
27
|
+
extra = kwargs.pop("json_schema_extra", {}) or {}
|
|
28
|
+
extra["sensitivity"] = sensitivity.value
|
|
29
|
+
return Field(default, title=title, json_schema_extra=extra, **kwargs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class YsModel(BaseModel):
|
|
33
|
+
"""全实体公共基类。"""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(populate_by_name=True, use_enum_values=False, extra="ignore")
|
|
36
|
+
|
|
37
|
+
id: str = Field(default="", alias="_id", title="主键")
|
|
38
|
+
created_at: float = Field(default_factory=time.time, title="创建时间")
|
|
39
|
+
updated_at: float = Field(default_factory=time.time, title="更新时间")
|
|
40
|
+
deleted_at: float | None = Field(default=None, title="软删除时间")
|
|
41
|
+
|
|
42
|
+
def to_doc(self) -> dict[str, Any]:
|
|
43
|
+
"""导出为存储/传输文档(含 _id 别名,枚举转值)。"""
|
|
44
|
+
return self.model_dump(by_alias=True, mode="json")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def field_sensitivity(model_cls: type[BaseModel], field_name: str) -> Sensitivity:
|
|
48
|
+
info = model_cls.model_fields.get(field_name)
|
|
49
|
+
if info is None:
|
|
50
|
+
return Sensitivity.public
|
|
51
|
+
extra = info.json_schema_extra
|
|
52
|
+
if isinstance(extra, dict):
|
|
53
|
+
value = extra.get("sensitivity")
|
|
54
|
+
if value:
|
|
55
|
+
return Sensitivity(value)
|
|
56
|
+
return Sensitivity.public
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fields_for_grant(model_cls: type[BaseModel], grant: FieldGrant) -> set[str]:
|
|
60
|
+
"""该授权等级下可见的字段名集合(含 _id 别名对应的 id)。"""
|
|
61
|
+
allowed = GRANT_ALLOWS[grant]
|
|
62
|
+
return {name for name in model_cls.model_fields if field_sensitivity(model_cls, name) in allowed}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def project_doc(model_cls: type[BaseModel], doc: dict[str, Any], grant: FieldGrant) -> dict[str, Any]:
|
|
66
|
+
"""按授权等级裁剪文档:高于授权的字段整体剔除(而非置空)。
|
|
67
|
+
|
|
68
|
+
doc 以存储形态(_id 别名)传入/返回;未在契约中的键一律剔除。
|
|
69
|
+
"""
|
|
70
|
+
visible = fields_for_grant(model_cls, grant)
|
|
71
|
+
out: dict[str, Any] = {}
|
|
72
|
+
for name in visible:
|
|
73
|
+
info = model_cls.model_fields[name]
|
|
74
|
+
key = info.alias or name
|
|
75
|
+
if key in doc:
|
|
76
|
+
out[key] = doc[key]
|
|
77
|
+
return out
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""契约枚举。代码值为 str;中文名/排序等展示元数据集中在 ENUM_META,经 export_enum_meta() 下发前端。"""
|
|
3
|
+
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserRole(StrEnum):
|
|
9
|
+
Observer = "Observer"
|
|
10
|
+
User = "User"
|
|
11
|
+
Operator = "Operator"
|
|
12
|
+
Manager = "Manager"
|
|
13
|
+
Root = "Root"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TeamType(StrEnum):
|
|
17
|
+
Manager = "Manager"
|
|
18
|
+
Commoner = "Commoner"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OwnerType(StrEnum):
|
|
22
|
+
Product = "Product"
|
|
23
|
+
LegalPerson = "LegalPerson"
|
|
24
|
+
NaturalPerson = "NaturalPerson"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ExchangeType(StrEnum):
|
|
28
|
+
SH = "SH"
|
|
29
|
+
SZ = "SZ"
|
|
30
|
+
SHFE = "SHFE"
|
|
31
|
+
CFFEX = "CFFEX"
|
|
32
|
+
CZCE = "CZCE"
|
|
33
|
+
DCE = "DCE"
|
|
34
|
+
GFEX = "GFEX"
|
|
35
|
+
INE = "INE"
|
|
36
|
+
SGE = "SGE"
|
|
37
|
+
UNKNOWN = "UNKNOWN"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AccountType(StrEnum):
|
|
41
|
+
STOCK = "STOCK"
|
|
42
|
+
FUTURES = "FUTURES"
|
|
43
|
+
CREDIT = "CREDIT"
|
|
44
|
+
STOCK_OPTION = "STOCK_OPTION"
|
|
45
|
+
GOLD = "GOLD"
|
|
46
|
+
INCOME_SWAP = "INCOME_SWAP"
|
|
47
|
+
FUTURES_OPTION = "FUTURES_OPTION"
|
|
48
|
+
HUGANGTONG = "HUGANGTONG"
|
|
49
|
+
NEW3BOARD = "NEW3BOARD"
|
|
50
|
+
SHENGANGTONG = "SHENGANGTONG"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TradeEnvType(StrEnum):
|
|
54
|
+
LIVE = "LIVE"
|
|
55
|
+
TEST = "TEST"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AccessTable(StrEnum):
|
|
59
|
+
"""表级订阅授权。"""
|
|
60
|
+
|
|
61
|
+
USER = "USER"
|
|
62
|
+
TEAM = "TEAM"
|
|
63
|
+
OWNER = "OWNER"
|
|
64
|
+
ACCOUNT = "ACCOUNT"
|
|
65
|
+
CUSTODIAN = "CUSTODIAN"
|
|
66
|
+
PROJECT = "PROJECT"
|
|
67
|
+
APP_CONFIG = "APP_CONFIG"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class GatewayMode(StrEnum):
|
|
71
|
+
db = "db"
|
|
72
|
+
flow = "flow"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ServerType(StrEnum):
|
|
76
|
+
td = "td"
|
|
77
|
+
md = "md"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AppKind(StrEnum):
|
|
81
|
+
service = "service" # 连 auth 订阅数据的子系统
|
|
82
|
+
link = "link" # 纯门户外链
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AppStatus(StrEnum):
|
|
86
|
+
enabled = "enabled"
|
|
87
|
+
disabled = "disabled"
|
|
88
|
+
maintenance = "maintenance"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FieldGrant(StrEnum):
|
|
92
|
+
"""字段级授权(层级递进:credential ⊃ restricted ⊃ public)。"""
|
|
93
|
+
|
|
94
|
+
public = "public"
|
|
95
|
+
restricted = "restricted"
|
|
96
|
+
credential = "credential"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Sensitivity(StrEnum):
|
|
100
|
+
"""契约字段敏感级标注。"""
|
|
101
|
+
|
|
102
|
+
public = "public"
|
|
103
|
+
restricted = "restricted" # 生产基础设施信息
|
|
104
|
+
credential = "credential" # 凭据
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ChangeOp(StrEnum):
|
|
108
|
+
upsert = "upsert"
|
|
109
|
+
delete = "delete"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
#: 层级:grant 允许看到的 sensitivity 集合
|
|
113
|
+
GRANT_ALLOWS: dict[FieldGrant, frozenset[Sensitivity]] = {
|
|
114
|
+
FieldGrant.public: frozenset({Sensitivity.public}),
|
|
115
|
+
FieldGrant.restricted: frozenset({Sensitivity.public, Sensitivity.restricted}),
|
|
116
|
+
FieldGrant.credential: frozenset({Sensitivity.public, Sensitivity.restricted, Sensitivity.credential}),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#: 展示元数据(前端经 export_enum_meta() 获取,替代旧 dict-value 枚举)
|
|
120
|
+
ENUM_META: dict[str, dict[str, dict[str, Any]]] = {
|
|
121
|
+
"UserRole": {
|
|
122
|
+
"Observer": {"name": "观察员", "sort": 0},
|
|
123
|
+
"User": {"name": "普通用户", "sort": 1},
|
|
124
|
+
"Operator": {"name": "运维员", "sort": 2},
|
|
125
|
+
"Manager": {"name": "管理员", "sort": 3},
|
|
126
|
+
"Root": {"name": "超级管理员", "sort": 4},
|
|
127
|
+
},
|
|
128
|
+
"TeamType": {
|
|
129
|
+
"Manager": {"name": "管理人", "sort": 0},
|
|
130
|
+
"Commoner": {"name": "非管理人", "sort": 1},
|
|
131
|
+
},
|
|
132
|
+
"OwnerType": {
|
|
133
|
+
"Product": {"name": "产品", "team_type": "Manager", "sort": 0},
|
|
134
|
+
"LegalPerson": {"name": "法人", "team_type": "Commoner", "sort": 1},
|
|
135
|
+
"NaturalPerson": {"name": "自然人", "team_type": "Commoner", "sort": 2},
|
|
136
|
+
},
|
|
137
|
+
"ExchangeType": {
|
|
138
|
+
"SH": {"name": "上交所", "market": "STOCK"},
|
|
139
|
+
"SZ": {"name": "深交所", "market": "STOCK"},
|
|
140
|
+
"SHFE": {"name": "上期所", "market": "FUTURES"},
|
|
141
|
+
"CFFEX": {"name": "中金所", "market": "FUTURES"},
|
|
142
|
+
"CZCE": {"name": "郑商所", "market": "FUTURES"},
|
|
143
|
+
"DCE": {"name": "大商所", "market": "FUTURES"},
|
|
144
|
+
"GFEX": {"name": "广期所", "market": "FUTURES"},
|
|
145
|
+
"INE": {"name": "上能所", "market": "FUTURES"},
|
|
146
|
+
"SGE": {"name": "金交所", "market": "FUTURES"},
|
|
147
|
+
"UNKNOWN": {"name": "其他", "market": "UNKNOWN"},
|
|
148
|
+
},
|
|
149
|
+
"AccountType": {
|
|
150
|
+
"STOCK": {"name": "股票账户", "agent": ["STOCK", "FUTURES"]},
|
|
151
|
+
"FUTURES": {"name": "期货账户", "agent": ["FUTURES"]},
|
|
152
|
+
"CREDIT": {"name": "信用账户", "agent": ["STOCK"]},
|
|
153
|
+
"STOCK_OPTION": {"name": "股票期权", "agent": ["STOCK", "FUTURES"]},
|
|
154
|
+
"GOLD": {"name": "贵金属账户", "agent": ["STOCK", "FUTURES"]},
|
|
155
|
+
"INCOME_SWAP": {"name": "收益互换账户", "agent": ["STOCK"]},
|
|
156
|
+
"FUTURES_OPTION": {"name": "期货期权", "agent": ["FUTURES"]},
|
|
157
|
+
"HUGANGTONG": {"name": "沪港通账户", "agent": ["STOCK"]},
|
|
158
|
+
"NEW3BOARD": {"name": "全国股转账户", "agent": ["STOCK"]},
|
|
159
|
+
"SHENGANGTONG": {"name": "深港通账户", "agent": ["STOCK"]},
|
|
160
|
+
},
|
|
161
|
+
"TradeEnvType": {
|
|
162
|
+
"LIVE": {"name": "实盘", "sort": 0},
|
|
163
|
+
"TEST": {"name": "测试", "sort": 1},
|
|
164
|
+
},
|
|
165
|
+
"AccessTable": {
|
|
166
|
+
"USER": {"name": "用户表", "sort": 0},
|
|
167
|
+
"TEAM": {"name": "团队表", "sort": 1},
|
|
168
|
+
"OWNER": {"name": "产品表", "sort": 2},
|
|
169
|
+
"ACCOUNT": {"name": "账户表", "sort": 3},
|
|
170
|
+
"CUSTODIAN": {"name": "托管表", "sort": 4},
|
|
171
|
+
"PROJECT": {"name": "项目表", "sort": 5},
|
|
172
|
+
"APP_CONFIG": {"name": "应用配置", "sort": 6},
|
|
173
|
+
},
|
|
174
|
+
"GatewayMode": {"db": {"name": "数据库"}, "flow": {"name": "流"}},
|
|
175
|
+
"ServerType": {"td": {"name": "交易"}, "md": {"name": "行情"}},
|
|
176
|
+
"AppKind": {"service": {"name": "业务子系统"}, "link": {"name": "门户外链"}},
|
|
177
|
+
"AppStatus": {
|
|
178
|
+
"enabled": {"name": "运行中"},
|
|
179
|
+
"disabled": {"name": "已停用"},
|
|
180
|
+
"maintenance": {"name": "维护中"},
|
|
181
|
+
},
|
|
182
|
+
"FieldGrant": {
|
|
183
|
+
"public": {"name": "常规"},
|
|
184
|
+
"restricted": {"name": "含生产信息"},
|
|
185
|
+
"credential": {"name": "含凭据"},
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def export_enum_meta() -> dict[str, Any]:
|
|
191
|
+
"""导出枚举元数据(成员顺序 + 展示信息),供前端与 codegen 使用。"""
|
|
192
|
+
import ys_base.contracts.enums as _self
|
|
193
|
+
|
|
194
|
+
result: dict[str, Any] = {}
|
|
195
|
+
for enum_name, meta in ENUM_META.items():
|
|
196
|
+
enum_cls = getattr(_self, enum_name)
|
|
197
|
+
result[enum_name] = {
|
|
198
|
+
"members": [m.value for m in enum_cls],
|
|
199
|
+
"meta": meta,
|
|
200
|
+
}
|
|
201
|
+
return result
|