engagelab-apppush 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EngageLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: engagelab-apppush
3
+ Version: 0.1.0
4
+ Summary: EngageLab AppPush REST API Python SDK (zero third-party dependencies)
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/engagelab-mt/engagelab-apppush-python
7
+ Project-URL: Repository, https://github.com/engagelab-mt/engagelab-apppush-python
8
+ Project-URL: Issues, https://github.com/engagelab-mt/engagelab-apppush-python/issues
9
+ Keywords: engagelab,push,notification,apppush,sdk
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # EngageLab AppPush Python SDK
27
+
28
+ EngageLab AppPush REST API 的 Python SDK,零第三方依赖,仅使用 Python 标准库。
29
+
30
+ ## 安装
31
+
32
+ ```bash
33
+ pip install engagelab-apppush
34
+ ```
35
+
36
+ 或从源码安装:
37
+
38
+ ```bash
39
+ pip install .
40
+ ```
41
+
42
+ ## 快速开始
43
+
44
+ ```python
45
+ import engagelab
46
+
47
+ # 创建客户端(默认新加坡数据中心)
48
+ client = engagelab.Client("your-app-key", "your-master-secret")
49
+
50
+ # 或指定数据中心
51
+ client = engagelab.Client(
52
+ "your-app-key",
53
+ "your-master-secret",
54
+ base_url=engagelab.DataCenter.HONG_KONG,
55
+ )
56
+
57
+ # 发送推送
58
+ result = client.push.send(engagelab.PushParam(
59
+ from_="push",
60
+ to="all",
61
+ body=engagelab.PushBody(
62
+ platform="all",
63
+ notification=engagelab.NotificationMessage(
64
+ alert="Hello from Python SDK!",
65
+ android=engagelab.AndroidNotification(
66
+ alert="Hello Android!",
67
+ title="Test Push",
68
+ ),
69
+ ios=engagelab.IOSNotification(
70
+ alert="Hello iOS!",
71
+ ),
72
+ ),
73
+ ),
74
+ ))
75
+ print(f"Push sent: msg_id={result.msg_id}")
76
+ ```
77
+
78
+ ## 数据中心
79
+
80
+ | 常量 | 区域 | Base URL |
81
+ | --------------------------- | ----------- | ------------------------------------- |
82
+ | `DataCenter.SINGAPORE` | 新加坡 (默认) | `https://pushapi-sgp.engagelab.com` |
83
+ | `DataCenter.HONG_KONG` | 香港 | `https://pushapi-hk.engagelab.com` |
84
+ | `DataCenter.VIRGINIA` | 美国弗吉尼亚 | `https://pushapi-usva.engagelab.com` |
85
+ | `DataCenter.FRANKFURT` | 德国法兰克福 | `https://pushapi-defra.engagelab.com` |
86
+
87
+ ## API 模块
88
+
89
+ ### Push — 推送
90
+
91
+ ```python
92
+ client.push.send(param) # 创建推送
93
+ client.push.send_raw(raw_body) # 自定义 JSON 推送
94
+ client.push.validate(param) # 推送校验
95
+ client.push.withdraw(msg_id) # 消息撤回
96
+ client.push.batch_by_regid(param) # 按 Registration ID 批量推送
97
+ client.push.batch_by_alias(param) # 按 Alias 批量推送
98
+ ```
99
+
100
+ ### Group Push — 应用分组推送
101
+
102
+ ```python
103
+ # Group Push 使用独立认证: group-{GroupKey}:{GroupMasterSecret}
104
+ group_client = engagelab.GroupPushClient(
105
+ "group-key",
106
+ "group-master-secret",
107
+ base_url=engagelab.DataCenter.SINGAPORE,
108
+ )
109
+ group_client.send(param)
110
+ ```
111
+
112
+ ### Device — 设备
113
+
114
+ ```python
115
+ client.device.get(registration_id) # 查询设备信息
116
+ client.device.set(registration_id, param) # 设置设备标签/别名
117
+ client.device.delete(registration_id) # 删除设备
118
+ client.device.get_status(param) # 查询设备在线状态
119
+ ```
120
+
121
+ ### Tag — 标签
122
+
123
+ ```python
124
+ client.tag.list() # 获取标签列表
125
+ client.tag.set(tag, param) # 添加/移除标签设备
126
+ client.tag.delete(tag, platforms=["android"]) # 删除标签
127
+ client.tag.get_count(tags, platforms=["android"]) # 查询标签设备数
128
+ client.tag.get_device_status(tag, registration_id) # 查询设备标签绑定状态
129
+ client.tag.get_quota(tags=["vip"], platforms=["android"]) # 查询标签配额
130
+ ```
131
+
132
+ ### Alias — 别名
133
+
134
+ ```python
135
+ client.alias.get(alias, platforms=["android"]) # 查询别名设备
136
+ client.alias.delete(alias, platforms=["android"]) # 删除别名
137
+ ```
138
+
139
+ ### Schedule — 定时任务
140
+
141
+ ```python
142
+ client.schedule.create(param) # 创建定时推送
143
+ client.schedule.update(id, param) # 更新定时推送
144
+ client.schedule.delete(id) # 删除定时推送
145
+ client.schedule.get(id) # 获取定时推送详情
146
+ client.schedule.list(page=1) # 获取定时推送列表
147
+ client.schedule.get_msg_ids(id) # 获取定时推送消息 ID
148
+ ```
149
+
150
+ ### Status — 统计
151
+
152
+ ```python
153
+ client.status.users(time_unit, start, duration) # 用户统计
154
+ client.status.message_detail(message_ids) # 消息送达统计
155
+ client.status.message_lifecycle(msg_id, registration_ids) # 消息生命周期
156
+ client.status.batch_message_detail(message_ids) # 批量消息统计
157
+ client.status.plan_detail(plan_id, message_ids) # 推送计划统计
158
+ ```
159
+
160
+ ### Plan — 推送计划
161
+
162
+ ```python
163
+ client.plan.create_or_update(param) # 创建/更新推送计划
164
+ client.plan.list(page_index=1, page_size=10, send_source=None, search_description="") # 查询列表
165
+ client.plan.query_msg(plan_ids, start_date, end_date) # 查询计划消息 ID
166
+ client.plan.delete(plan_id) # 删除推送计划
167
+ client.plan.batch_delete(plan_ids) # 批量删除推送计划
168
+ ```
169
+
170
+ ### Voice — 语音/TTS
171
+
172
+ ```python
173
+ client.voice.create(param) # 创建语音模板
174
+ client.voice.list() # 获取语音模板列表
175
+ client.voice.get(language) # 获取语音模板
176
+ client.voice.delete(language) # 删除语音模板
177
+ ```
178
+
179
+ ### Image — 图片
180
+
181
+ ```python
182
+ client.image.upload_oppo(file_path) # 上传 OPPO 大图 (文件路径)
183
+ client.image.upload_oppo_from_reader(filename, reader) # 上传 OPPO 大图 (file-like object)
184
+ ```
185
+
186
+ ## 错误处理
187
+
188
+ SDK 使用 `engagelab.ApiError` 异常类型返回 API 错误:
189
+
190
+ ```python
191
+ try:
192
+ result = client.push.send(param)
193
+ except engagelab.ApiError as e:
194
+ print(f"API Error: status={e.status_code} code={e.error.code} message={e.error.message}")
195
+ except Exception as e:
196
+ print(f"Network Error: {e}")
197
+ ```
198
+
199
+ ## 客户端配置
200
+
201
+ ```python
202
+ # 指定数据中心
203
+ client = engagelab.Client("key", "secret",
204
+ base_url=engagelab.DataCenter.HONG_KONG,
205
+ )
206
+
207
+ # 自定义超时
208
+ client = engagelab.Client("key", "secret", timeout=60)
209
+
210
+ # 自定义 Base URL
211
+ client = engagelab.Client("key", "secret",
212
+ base_url="https://custom-api.example.com",
213
+ )
214
+ ```
215
+
216
+ ## 认证方式
217
+
218
+ | API 模块 | 认证 |
219
+ | ---------------------------------------------------------------------- | ----------------------------------------------- |
220
+ | Push / Device / Tag / Alias / Schedule / Status / Plan / Voice / Image | Basic Auth: `appKey:masterSecret` |
221
+ | Group Push | Basic Auth: `group-{GroupKey}:GroupMasterSecret` |
222
+
223
+ ## 更新日志
224
+
225
+ 详见 [CHANGELOG.md](CHANGELOG.md)。
226
+
227
+ ## 许可证
228
+
229
+ MIT License
@@ -0,0 +1,204 @@
1
+ # EngageLab AppPush Python SDK
2
+
3
+ EngageLab AppPush REST API 的 Python SDK,零第三方依赖,仅使用 Python 标准库。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install engagelab-apppush
9
+ ```
10
+
11
+ 或从源码安装:
12
+
13
+ ```bash
14
+ pip install .
15
+ ```
16
+
17
+ ## 快速开始
18
+
19
+ ```python
20
+ import engagelab
21
+
22
+ # 创建客户端(默认新加坡数据中心)
23
+ client = engagelab.Client("your-app-key", "your-master-secret")
24
+
25
+ # 或指定数据中心
26
+ client = engagelab.Client(
27
+ "your-app-key",
28
+ "your-master-secret",
29
+ base_url=engagelab.DataCenter.HONG_KONG,
30
+ )
31
+
32
+ # 发送推送
33
+ result = client.push.send(engagelab.PushParam(
34
+ from_="push",
35
+ to="all",
36
+ body=engagelab.PushBody(
37
+ platform="all",
38
+ notification=engagelab.NotificationMessage(
39
+ alert="Hello from Python SDK!",
40
+ android=engagelab.AndroidNotification(
41
+ alert="Hello Android!",
42
+ title="Test Push",
43
+ ),
44
+ ios=engagelab.IOSNotification(
45
+ alert="Hello iOS!",
46
+ ),
47
+ ),
48
+ ),
49
+ ))
50
+ print(f"Push sent: msg_id={result.msg_id}")
51
+ ```
52
+
53
+ ## 数据中心
54
+
55
+ | 常量 | 区域 | Base URL |
56
+ | --------------------------- | ----------- | ------------------------------------- |
57
+ | `DataCenter.SINGAPORE` | 新加坡 (默认) | `https://pushapi-sgp.engagelab.com` |
58
+ | `DataCenter.HONG_KONG` | 香港 | `https://pushapi-hk.engagelab.com` |
59
+ | `DataCenter.VIRGINIA` | 美国弗吉尼亚 | `https://pushapi-usva.engagelab.com` |
60
+ | `DataCenter.FRANKFURT` | 德国法兰克福 | `https://pushapi-defra.engagelab.com` |
61
+
62
+ ## API 模块
63
+
64
+ ### Push — 推送
65
+
66
+ ```python
67
+ client.push.send(param) # 创建推送
68
+ client.push.send_raw(raw_body) # 自定义 JSON 推送
69
+ client.push.validate(param) # 推送校验
70
+ client.push.withdraw(msg_id) # 消息撤回
71
+ client.push.batch_by_regid(param) # 按 Registration ID 批量推送
72
+ client.push.batch_by_alias(param) # 按 Alias 批量推送
73
+ ```
74
+
75
+ ### Group Push — 应用分组推送
76
+
77
+ ```python
78
+ # Group Push 使用独立认证: group-{GroupKey}:{GroupMasterSecret}
79
+ group_client = engagelab.GroupPushClient(
80
+ "group-key",
81
+ "group-master-secret",
82
+ base_url=engagelab.DataCenter.SINGAPORE,
83
+ )
84
+ group_client.send(param)
85
+ ```
86
+
87
+ ### Device — 设备
88
+
89
+ ```python
90
+ client.device.get(registration_id) # 查询设备信息
91
+ client.device.set(registration_id, param) # 设置设备标签/别名
92
+ client.device.delete(registration_id) # 删除设备
93
+ client.device.get_status(param) # 查询设备在线状态
94
+ ```
95
+
96
+ ### Tag — 标签
97
+
98
+ ```python
99
+ client.tag.list() # 获取标签列表
100
+ client.tag.set(tag, param) # 添加/移除标签设备
101
+ client.tag.delete(tag, platforms=["android"]) # 删除标签
102
+ client.tag.get_count(tags, platforms=["android"]) # 查询标签设备数
103
+ client.tag.get_device_status(tag, registration_id) # 查询设备标签绑定状态
104
+ client.tag.get_quota(tags=["vip"], platforms=["android"]) # 查询标签配额
105
+ ```
106
+
107
+ ### Alias — 别名
108
+
109
+ ```python
110
+ client.alias.get(alias, platforms=["android"]) # 查询别名设备
111
+ client.alias.delete(alias, platforms=["android"]) # 删除别名
112
+ ```
113
+
114
+ ### Schedule — 定时任务
115
+
116
+ ```python
117
+ client.schedule.create(param) # 创建定时推送
118
+ client.schedule.update(id, param) # 更新定时推送
119
+ client.schedule.delete(id) # 删除定时推送
120
+ client.schedule.get(id) # 获取定时推送详情
121
+ client.schedule.list(page=1) # 获取定时推送列表
122
+ client.schedule.get_msg_ids(id) # 获取定时推送消息 ID
123
+ ```
124
+
125
+ ### Status — 统计
126
+
127
+ ```python
128
+ client.status.users(time_unit, start, duration) # 用户统计
129
+ client.status.message_detail(message_ids) # 消息送达统计
130
+ client.status.message_lifecycle(msg_id, registration_ids) # 消息生命周期
131
+ client.status.batch_message_detail(message_ids) # 批量消息统计
132
+ client.status.plan_detail(plan_id, message_ids) # 推送计划统计
133
+ ```
134
+
135
+ ### Plan — 推送计划
136
+
137
+ ```python
138
+ client.plan.create_or_update(param) # 创建/更新推送计划
139
+ client.plan.list(page_index=1, page_size=10, send_source=None, search_description="") # 查询列表
140
+ client.plan.query_msg(plan_ids, start_date, end_date) # 查询计划消息 ID
141
+ client.plan.delete(plan_id) # 删除推送计划
142
+ client.plan.batch_delete(plan_ids) # 批量删除推送计划
143
+ ```
144
+
145
+ ### Voice — 语音/TTS
146
+
147
+ ```python
148
+ client.voice.create(param) # 创建语音模板
149
+ client.voice.list() # 获取语音模板列表
150
+ client.voice.get(language) # 获取语音模板
151
+ client.voice.delete(language) # 删除语音模板
152
+ ```
153
+
154
+ ### Image — 图片
155
+
156
+ ```python
157
+ client.image.upload_oppo(file_path) # 上传 OPPO 大图 (文件路径)
158
+ client.image.upload_oppo_from_reader(filename, reader) # 上传 OPPO 大图 (file-like object)
159
+ ```
160
+
161
+ ## 错误处理
162
+
163
+ SDK 使用 `engagelab.ApiError` 异常类型返回 API 错误:
164
+
165
+ ```python
166
+ try:
167
+ result = client.push.send(param)
168
+ except engagelab.ApiError as e:
169
+ print(f"API Error: status={e.status_code} code={e.error.code} message={e.error.message}")
170
+ except Exception as e:
171
+ print(f"Network Error: {e}")
172
+ ```
173
+
174
+ ## 客户端配置
175
+
176
+ ```python
177
+ # 指定数据中心
178
+ client = engagelab.Client("key", "secret",
179
+ base_url=engagelab.DataCenter.HONG_KONG,
180
+ )
181
+
182
+ # 自定义超时
183
+ client = engagelab.Client("key", "secret", timeout=60)
184
+
185
+ # 自定义 Base URL
186
+ client = engagelab.Client("key", "secret",
187
+ base_url="https://custom-api.example.com",
188
+ )
189
+ ```
190
+
191
+ ## 认证方式
192
+
193
+ | API 模块 | 认证 |
194
+ | ---------------------------------------------------------------------- | ----------------------------------------------- |
195
+ | Push / Device / Tag / Alias / Schedule / Status / Plan / Voice / Image | Basic Auth: `appKey:masterSecret` |
196
+ | Group Push | Basic Auth: `group-{GroupKey}:GroupMasterSecret` |
197
+
198
+ ## 更新日志
199
+
200
+ 详见 [CHANGELOG.md](CHANGELOG.md)。
201
+
202
+ ## 许可证
203
+
204
+ MIT License
@@ -0,0 +1,157 @@
1
+ """EngageLab AppPush Python SDK — zero third-party dependencies.
2
+
3
+ Quick start::
4
+
5
+ import engagelab
6
+
7
+ client = engagelab.Client("app_key", "master_secret")
8
+ result = client.push.send(engagelab.PushParam(
9
+ to="all",
10
+ body=engagelab.PushBody(
11
+ platform="all",
12
+ notification=engagelab.NotificationMessage(alert="Hello!"),
13
+ ),
14
+ ))
15
+ print(result.msg_id)
16
+
17
+ For Group Push (separate authentication)::
18
+
19
+ gc = engagelab.GroupPushClient("group_key", "group_master_secret")
20
+ result = gc.send(engagelab.PushParam(...))
21
+ """
22
+
23
+ from .client import Client, DataCenter
24
+ from .errors import ApiError, ErrorDetail
25
+ from .push import (
26
+ AndroidIntent,
27
+ AndroidNotification,
28
+ BatchPushParam,
29
+ BatchPushRequest,
30
+ BatchPushSingleResult,
31
+ CustomMessage,
32
+ HmosIntent,
33
+ HmosNotification,
34
+ IOSNotification,
35
+ LiveActivityAlert,
36
+ LiveActivityIOS,
37
+ LiveActivityMessage,
38
+ NotificationMessage,
39
+ Options,
40
+ PushBody,
41
+ PushParam,
42
+ PushResult,
43
+ PushTo,
44
+ PushWithdrawResult,
45
+ Seg,
46
+ )
47
+ from .device import (
48
+ DeviceGetResult,
49
+ DeviceSetParam,
50
+ DeviceSetTags,
51
+ DeviceStatusGetParam,
52
+ DeviceStatusGetResult,
53
+ )
54
+ from .tag import (
55
+ TagQuotaData,
56
+ TagQuotaGetResult,
57
+ TagRegistrationIDs,
58
+ TagSetParam,
59
+ TagsCountGetResult,
60
+ TagsGetResult,
61
+ )
62
+ from .alias import AliasStatusGetResult
63
+ from .schedule import (
64
+ SchedulePushDetailGetResult,
65
+ SchedulePushGetResult,
66
+ SchedulePushListResult,
67
+ SchedulePushParam,
68
+ SchedulePushResult,
69
+ ScheduleTrigger,
70
+ TriggerPeriodical,
71
+ TriggerSingle,
72
+ )
73
+ from .status import UserStatusGetResult, UserStatusItem, UserStatusPlatform
74
+ from .plan import (
75
+ PushPlanDeleteResult,
76
+ PushPlanInfo,
77
+ PushPlanListResult,
78
+ PushPlanParam,
79
+ PushPlanResult,
80
+ )
81
+ from .voice import VoiceListResult, VoiceParam, VoiceResult
82
+ from .image import ImageUploadResult
83
+ from .group_push import GroupPushClient, GroupPushResult
84
+
85
+ __version__ = "0.1.0"
86
+
87
+ __all__ = [
88
+ # Core
89
+ "Client",
90
+ "DataCenter",
91
+ "ApiError",
92
+ "ErrorDetail",
93
+ # Push
94
+ "PushParam",
95
+ "PushBody",
96
+ "PushTo",
97
+ "Seg",
98
+ "NotificationMessage",
99
+ "AndroidNotification",
100
+ "AndroidIntent",
101
+ "IOSNotification",
102
+ "HmosNotification",
103
+ "HmosIntent",
104
+ "CustomMessage",
105
+ "LiveActivityMessage",
106
+ "LiveActivityIOS",
107
+ "LiveActivityAlert",
108
+ "Options",
109
+ "PushResult",
110
+ "PushWithdrawResult",
111
+ "BatchPushParam",
112
+ "BatchPushRequest",
113
+ "BatchPushSingleResult",
114
+ # Device
115
+ "DeviceStatusGetParam",
116
+ "DeviceStatusGetResult",
117
+ "DeviceGetResult",
118
+ "DeviceSetParam",
119
+ "DeviceSetTags",
120
+ # Tag
121
+ "TagsGetResult",
122
+ "TagSetParam",
123
+ "TagRegistrationIDs",
124
+ "TagsCountGetResult",
125
+ "TagQuotaGetResult",
126
+ "TagQuotaData",
127
+ # Alias
128
+ "AliasStatusGetResult",
129
+ # Schedule
130
+ "SchedulePushParam",
131
+ "ScheduleTrigger",
132
+ "TriggerSingle",
133
+ "TriggerPeriodical",
134
+ "SchedulePushResult",
135
+ "SchedulePushGetResult",
136
+ "SchedulePushListResult",
137
+ "SchedulePushDetailGetResult",
138
+ # Status
139
+ "UserStatusGetResult",
140
+ "UserStatusItem",
141
+ "UserStatusPlatform",
142
+ # Plan
143
+ "PushPlanParam",
144
+ "PushPlanResult",
145
+ "PushPlanDeleteResult",
146
+ "PushPlanListResult",
147
+ "PushPlanInfo",
148
+ # Voice
149
+ "VoiceParam",
150
+ "VoiceResult",
151
+ "VoiceListResult",
152
+ # Image
153
+ "ImageUploadResult",
154
+ # Group Push
155
+ "GroupPushClient",
156
+ "GroupPushResult",
157
+ ]
@@ -0,0 +1,49 @@
1
+ """Internal serialization utilities for converting between Python objects and JSON-compatible dicts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from typing import Any, Dict, Type, TypeVar
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ def to_dict(obj: Any) -> Any:
12
+ """Recursively convert a dataclass instance (or dict/list) to a JSON-serializable dict.
13
+
14
+ ``None`` values are omitted, mirroring Go's ``omitempty`` JSON tag behaviour.
15
+ """
16
+ if obj is None:
17
+ return None
18
+ if isinstance(obj, dict):
19
+ return {k: to_dict(v) for k, v in obj.items() if v is not None}
20
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
21
+ result: Dict[str, Any] = {}
22
+ field_map: Dict[str, str] = getattr(type(obj), "_FIELD_MAP", {})
23
+ for f in dataclasses.fields(obj):
24
+ val = getattr(obj, f.name)
25
+ if val is None:
26
+ continue
27
+ key = field_map.get(f.name, f.name)
28
+ result[key] = to_dict(val)
29
+ return result
30
+ if isinstance(obj, (list, tuple)):
31
+ return [to_dict(v) for v in obj]
32
+ return obj
33
+
34
+
35
+ def from_dict(cls: Type[T], data: Any) -> T: # type: ignore[return]
36
+ """Create a dataclass instance from a dict, mapping JSON keys back to field names."""
37
+ if data is None:
38
+ return None # type: ignore[return-value]
39
+ if not dataclasses.is_dataclass(cls):
40
+ return data # type: ignore[return-value]
41
+ field_map: Dict[str, str] = getattr(cls, "_FIELD_MAP", {})
42
+ kwargs: Dict[str, Any] = {}
43
+ for f in dataclasses.fields(cls):
44
+ json_key = field_map.get(f.name, f.name)
45
+ if json_key in data:
46
+ kwargs[f.name] = data[json_key]
47
+ elif f.name in data:
48
+ kwargs[f.name] = data[f.name]
49
+ return cls(**kwargs)