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 ADDED
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ """YS 统一 SDK:契约模型 / auth 客户端与差量同步 / 应用服务骨架 / 交易网关。"""
3
+
4
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ from ys_base.auth.client import AppCredentials, AuthClient
3
+ from ys_base.auth.delta import DeltaSync
4
+
5
+ __all__ = ["AppCredentials", "AuthClient", "DeltaSync"]
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