py-chanjet-toolkit 1.0.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.
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: UTF-8 -*-
3
+ """
4
+ py_chajet 包入口模块
5
+
6
+ 该包是畅捷通 T+ 系列产品的 Python 客户端库,提供与畅捷通 T+ 系统的 API 交互能力。
7
+
8
+ 包结构:
9
+ - py_chajet/
10
+ - tplus/ # T+ 系列产品模块
11
+ - zkhb/ # 智慧社区业务模块
12
+ - __init__.py # 智慧社区服务客户端类
13
+ - utils.py # 工具函数集合
14
+
15
+ 主要功能:
16
+ - 提供畅捷通 T+ 智慧社区 WebService 接口的同步和异步调用
17
+ - 封装 SOAP 请求的构建和响应解析
18
+ - 提供数据校验、日期处理、SQL 生成等辅助工具函数
19
+
20
+ 使用示例:
21
+ from py_chajet.tplus.zkhb import ForcelandEstateService
22
+
23
+ service = ForcelandEstateService(base_url="http://your-server/estate")
24
+ response = service.get_data_set()
25
+ data = convert_to_get_data_set_results(response)
26
+ """
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: UTF-8 -*-
3
+ """
4
+ py_chajet.tplus 模块
5
+
6
+ 该模块包含畅捷通 T+ 系列产品的客户端实现,目前主要包含智慧社区(zkhb)业务模块。
7
+
8
+ 子模块:
9
+ - zkhb: 智慧社区业务模块,提供与智慧社区系统的 WebService 接口交互能力
10
+
11
+ 未来扩展计划:
12
+ - 添加更多 T+ 产品线的客户端支持
13
+ - 提供统一的 API 调用接口
14
+ - 增加数据模型和类型定义
15
+ """
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: UTF-8 -*-
3
+ from typing import Optional
4
+
5
+ import httpx
6
+ from py_httpx_toolkit import Httpx
7
+
8
+
9
+ class ForcelandEstateService(Httpx):
10
+ """
11
+ 畅捷通T+智慧社区服务客户端
12
+
13
+ 该类封装了与畅捷通T+智慧社区系统的WebService接口交互,提供同步和异步两种调用方式。
14
+ 通过SOAP协议与服务端通信,主要用于获取数据集信息。
15
+
16
+ 核心功能:
17
+ - 支持同步和异步HTTP客户端
18
+ - 封装GetDataSet接口调用
19
+ - 自动处理请求头和参数配置
20
+ - 支持自定义客户端配置
21
+
22
+ 典型应用场景:
23
+ - 查询物业收费数据
24
+ - 获取小区信息
25
+ - 查询房间详情
26
+ - 获取收费项目列表
27
+
28
+ Attributes:
29
+ base_url: 服务端基础URL
30
+ client_kwargs: HTTP客户端配置参数
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ base_url: Optional[str] = None,
36
+ client_kwargs: Optional[dict] = None,
37
+ ):
38
+ """
39
+ 初始化智慧社区服务客户端
40
+
41
+ Args:
42
+ base_url: WebService服务基础URL,如 "http://example.com/estate"
43
+ client_kwargs: 额外的HTTP客户端配置参数,如headers、timeout等
44
+
45
+ Note:
46
+ - base_url末尾的斜杠会被自动去除
47
+ - 默认超时时间为60秒
48
+ - 默认关闭SSL证书验证
49
+ """
50
+ # 处理base_url,去除末尾斜杠(如果存在)
51
+ self.base_url = base_url or ""
52
+ self.base_url = self.base_url[:-1] if self.base_url.endswith("/") else self.base_url
53
+
54
+ # 合并默认配置与用户配置
55
+ self.client_kwargs = client_kwargs or {}
56
+ self.client_kwargs = {
57
+ "base_url": self.base_url,
58
+ "timeout": 60, # 默认超时时间60秒
59
+ "verify": False, # 默认关闭SSL验证(适用于内部服务)
60
+ **self.client_kwargs,
61
+ }
62
+
63
+ def get_data_set(
64
+ self,
65
+ client: Optional[httpx.Client] = None,
66
+ client_kwargs: Optional[dict] = None,
67
+ **kwargs
68
+ ):
69
+ """
70
+ 同步调用GetDataSet接口
71
+
72
+ 通过SOAP协议请求智慧社区服务端的GetDataSet方法,获取数据集信息。
73
+
74
+ Args:
75
+ client: 可选的HTTP客户端实例,如果提供则使用该客户端;否则创建新客户端
76
+ client_kwargs: 可选的HTTP客户端配置参数,如headers、timeout等
77
+ **kwargs: 额外的请求参数,会覆盖默认配置
78
+
79
+ Returns:
80
+ httpx.Response: HTTP响应对象,包含XML格式的SOAP响应
81
+
82
+ Note:
83
+ - 默认Content-Type为text/xml; charset=utf-8
84
+ - 默认请求路径为 /estate/webService/ForcelandEstateService.asmx
85
+ - 默认HTTP方法为POST
86
+ """
87
+ # 合并默认参数与用户参数,用户参数优先级更高
88
+ client = client or None
89
+ client_kwargs = client_kwargs or {}
90
+ kwargs = kwargs or {}
91
+ kwargs = {
92
+ "url": "/estate/webService/ForcelandEstateService.asmx", # WebService接口路径
93
+ "method": "POST", # SOAP请求使用POST方法
94
+ "params": {
95
+ "op": "GetDataSet", # 操作名称
96
+ },
97
+ "headers": {
98
+ "Content-Type": "text/xml; charset=utf-8", # SOAP请求必须的Content-Type
99
+ },
100
+ **kwargs,
101
+ }
102
+
103
+ return self.request(client=client, client_kwargs=client_kwargs, **kwargs)
104
+
105
+ async def async_get_data_set(
106
+ self,
107
+ client: Optional[httpx.AsyncClient] = None,
108
+ client_kwargs: Optional[dict] = None,
109
+ **kwargs
110
+ ):
111
+ """
112
+ 异步调用GetDataSet接口
113
+
114
+ 通过SOAP协议异步请求智慧社区服务端的GetDataSet方法,获取数据集信息。
115
+
116
+ Args:
117
+ client: 可选的异步HTTP客户端实例,如果提供则使用该客户端;否则创建新客户端
118
+ client_kwargs: 可选的异步HTTP客户端配置参数,如headers、timeout等
119
+ **kwargs: 额外的请求参数,会覆盖默认配置
120
+
121
+ Returns:
122
+ httpx.Response: HTTP响应对象,包含XML格式的SOAP响应
123
+
124
+ Note:
125
+ - 默认Content-Type为text/xml; charset=utf-8
126
+ - 默认请求路径为 /estate/webService/ForcelandEstateService.asmx
127
+ - 默认HTTP方法为POST
128
+ """
129
+ # 合并默认参数与用户参数,用户参数优先级更高
130
+ client = client or None
131
+ client_kwargs = client_kwargs or {}
132
+ kwargs = kwargs or {}
133
+ kwargs = {
134
+ "url": "/estate/webService/ForcelandEstateService.asmx", # WebService接口路径
135
+ "method": "POST", # SOAP请求使用POST方法
136
+ "params": {
137
+ "op": "GetDataSet", # 操作名称
138
+ },
139
+ "headers": {
140
+ "Content-Type": "text/xml; charset=utf-8", # SOAP请求必须的Content-Type
141
+ },
142
+ **kwargs,
143
+ }
144
+
145
+ return await self.async_request(client=client, client_kwargs=client_kwargs, **kwargs)
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: UTF-8 -*-
3
+ from datetime import datetime
4
+
5
+ from pydantic import BaseModel, Field, AwareDatetime
6
+
7
+
8
+ class ActualPaymentItem(BaseModel):
9
+ ChargeMListID: int = Field(..., title="收费项目列表ID", description="收费项目列表ID")
10
+ ChargeMListNo: str = Field(..., title="收费项目列表编号", description="收费项目列表编号")
11
+ ChargeTime: datetime = Field(..., title="收费时间", description="收费时间")
12
+ PayerName: str = Field(..., title="缴费人姓名", description="缴费人姓名")
13
+ ChargePersonName: str = Field(..., title="收费人姓名", description="收费人姓名")
14
+ ActualPayMoney: float = Field(..., title="实际支付金额", description="实际支付金额")
15
+ ActualAmount: float = Field(..., title="实际支付金额", description="实际支付金额")
16
+ EstateID: int = Field(..., title="小区ID", description="小区ID")
17
+ EstateName: str = Field(..., title="小区名称", description="小区名称")
18
+ ItemName: str = Field(..., title="收费项目名称", description="收费项目名称")
19
+ ItemNames: str = Field(..., title="收费项目名称列表", description="收费项目名称列表")
20
+ ChargeFeeItemID: int = Field(..., title="收费项目ID", description="收费项目ID")
21
+ SDate: AwareDatetime = Field(..., title="开始日期", description="开始日期")
22
+ EDate: AwareDatetime = Field(..., title="结束日期", description="结束日期")
23
+ RmId: int = Field(..., title="房间ID", description="房间ID")
24
+ RmNo: str = Field(..., title="房间编号", description="房间编号")
25
+ IsPayFull: bool = Field(..., title="是否全付", description="是否全付")
26
+ PayDays: int = Field(..., title="支付天数", description="支付天数")
27
+ CreateTime: AwareDatetime = Field(..., title="开始日期", description="开始日期")
28
+ LastUpdateTime: AwareDatetime = Field(..., title="结束日期", description="结束日期")
29
+
30
+ model_config = {
31
+ "extra": "allow" # 允许额外动态字段,适配 API 响应的灵活性
32
+ }
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: UTF-8 -*-
3
+ from collections import Counter, defaultdict
4
+ from datetime import tzinfo
5
+ from typing import Any, Optional, Union
6
+
7
+ import arrow
8
+ import httpx
9
+ from jsonpath_ng import parse
10
+ from jsonschema.validators import Draft202012Validator
11
+ import xmltodict
12
+
13
+
14
+ def json_find_first(expression: str, data: Any) -> Any:
15
+ """
16
+ 使用 JSONPath 表达式从嵌套的 JSON 数据中查找第一个匹配项
17
+
18
+ JSONPath 是一种用于查询 JSON 结构的表达式语言,类似于 XPath 之于 XML。
19
+ 该函数通过解析表达式并在数据中执行查询,返回第一个匹配的值。
20
+
21
+ 典型应用场景:
22
+ - 从复杂的 API 响应中提取特定字段
23
+ - 在嵌套结构中快速定位目标数据
24
+ - 动态提取不确定位置的数据
25
+
26
+ Args:
27
+ expression: JSONPath 表达式字符串,用于指定数据查询路径
28
+ 示例表达式:
29
+ - "$.name" - 获取根节点下的 name 字段
30
+ - "$.users[0].id" - 获取 users 数组第一个元素的 id
31
+ - "$..id" - 获取所有层级下的 id 字段
32
+ data: 待查询的 JSON 数据,可以是字典、列表或任意嵌套结构
33
+
34
+ Returns:
35
+ Any: 第一个匹配到的值,如果表达式无效或没有找到匹配项则返回 None
36
+
37
+ Examples:
38
+ >>> data = {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}
39
+ >>> json_find_first("$.users[0].name", data)
40
+ 'Alice'
41
+ >>> json_find_first("$..id", data)
42
+ 1
43
+ """
44
+ # 使用 jsonpath_ng 库解析表达式并执行查询
45
+ # parse() 将表达式字符串转换为可执行的 JSONPath 对象
46
+ # find() 在数据中查找所有匹配项,返回 Match 对象列表
47
+ # 列表推导式提取每个匹配项的 value 属性
48
+ results = [i.value for i in parse(expression).find(data)]
49
+
50
+ # 检查查询结果是否为非空列表
51
+ # 如果有匹配项,返回第一个;否则返回 None
52
+ if isinstance(results, list) and len(results) > 0:
53
+ return results[0]
54
+ return None
55
+
56
+
57
+ def json_is_valid(schema: Optional[dict], data: Any) -> bool:
58
+ """
59
+ 校验 JSON 数据是否符合指定的 JSON Schema 规范
60
+
61
+ 使用 JSON Schema Draft 2020-12 版本进行校验,确保数据结构和类型符合预期。
62
+ 该函数常用于 API 响应数据的完整性校验,防止因数据格式异常导致的程序错误。
63
+
64
+ 典型应用场景:
65
+ - 验证登录响应是否包含必要的用户信息字段
66
+ - 校验 API 返回的数据结构是否符合预期
67
+ - 在数据处理前进行格式检查,提高代码健壮性
68
+
69
+ Args:
70
+ schema: JSON Schema 字典,定义了数据应遵循的结构规范
71
+ Schema 应符合 JSON Schema Draft 2020-12 规范
72
+ 可以包含 type、properties、required、minLength 等关键字
73
+ data: 待校验的 JSON 数据,可以是任意类型
74
+
75
+ Returns:
76
+ bool: 校验结果,True 表示数据符合 Schema,False 表示不符合
77
+
78
+ Examples:
79
+ >>> schema = {
80
+ ... "type": "object",
81
+ ... "properties": {"id": {"type": "string"}},
82
+ ... "required": ["id"]
83
+ ... }
84
+ >>> json_is_valid(schema, {"id": "123"})
85
+ True
86
+ >>> json_is_valid(schema, {"name": "Alice"})
87
+ False
88
+ """
89
+ # 使用 Draft202012Validator 创建校验器实例
90
+ # is_valid() 方法执行完整校验并返回布尔结果
91
+ return Draft202012Validator(schema).is_valid(data)
92
+
93
+
94
+ def build_get_data_set_post_xml(data: Optional[dict] = None) -> str:
95
+ """
96
+ 将请求数据转换为 SOAP GetDataSet 接口所需的 XML 格式
97
+
98
+ 该函数用于构建符合畅捷通 T+ 智慧社区 WebService 规范的 SOAP 请求体。
99
+ 将用户提供的查询参数(如 sql、url)封装到标准的 SOAP Envelope 结构中。
100
+
101
+ Args:
102
+ data: 可选的请求参数字典,可包含 sql、url 等字段
103
+
104
+ Returns:
105
+ str: 完整的 SOAP XML 请求体字符串
106
+
107
+ Note:
108
+ - 默认添加 sql 和 url 字段,值为空字符串
109
+ - 用户传入的数据会覆盖默认值
110
+ - 返回的 XML 包含标准的 SOAP 命名空间声明
111
+
112
+ Examples:
113
+ >>> build_get_data_set_post_data({"sql": "SELECT * FROM EstateDetail"})
114
+ '<?xml version="1.0" encoding="utf-8"?><soap:Envelope ...><GetDataSet><sql>SELECT * FROM EstateDetail</sql><url/></GetDataSet></soap:Envelope>'
115
+ """
116
+ # 处理默认参数,确保 data 不为 None
117
+ data = data or {}
118
+
119
+ # 合并默认字段与用户字段,用户字段优先级更高
120
+ data = {
121
+ "sql": "", # 默认空SQL语句
122
+ "url": "", # 默认空URL
123
+ **data, # 用户传入的数据覆盖默认值
124
+ }
125
+
126
+ # 使用 xmltodict 将字典转换为 XML 字符串
127
+ return xmltodict.unparse(
128
+ {
129
+ "soap:Envelope": {
130
+ # SOAP 标准命名空间声明
131
+ "@xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
132
+ # XML Schema 实例命名空间
133
+ "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
134
+ # XML Schema 命名空间
135
+ "@xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
136
+ "soap:Body": {
137
+ "GetDataSet": {
138
+ # 畅捷通智慧社区 WebService 命名空间
139
+ "@xmlns": "http://zkhb.com.cn/",
140
+ **data, # 展开用户提供的请求数据
141
+ }
142
+ },
143
+ }
144
+ }
145
+ )
146
+
147
+
148
+ def build_get_data_set_results(response: Optional[Any] = None) -> list:
149
+ """
150
+ 从 GetDataSet 接口响应中提取实际数据列表
151
+
152
+ 该函数用于解析畅捷通 T+ 智慧社区 API 返回的 SOAP 响应,提取嵌套在复杂 XML 结构中的实际业务数据。
153
+ 支持直接传入 httpx.Response 对象或已解析的 XML 字典。
154
+
155
+ 数据提取路径说明:
156
+ SOAP响应 -> Envelope -> Body -> GetDataSetResponse -> GetDataSetResult -> diffgram -> NewDataSet -> Table
157
+
158
+ Args:
159
+ response: 从畅捷通 tplus API 返回的响应数据
160
+ 可以是 httpx.Response 对象,也可以是已解析的 XML 字典
161
+
162
+ Returns:
163
+ list: 包含实际业务数据的列表,每个元素是一个字典
164
+
165
+ Note:
166
+ - 如果提取结果是单个字典(单行数据),自动包装为列表
167
+ - 如果未找到匹配数据,返回空列表
168
+
169
+ Examples:
170
+ >>> # 假设 response 是包含单行数据的响应
171
+ >>> build_get_data_set_results(response)
172
+ [{'ChargeMListID': '1', 'ChargeMListNo': 'CF20240101'}]
173
+ """
174
+ results = []
175
+
176
+ # 根据响应类型选择不同的解析方式
177
+ if isinstance(response, httpx.Response):
178
+ # 如果是 httpx.Response 对象,先解析 XML 为字典
179
+ results = json_find_first(
180
+ "$.'soap:Envelope'.'soap:Body'.GetDataSetResponse.GetDataSetResult.'diffgr:diffgram'.NewDataSet.Table",
181
+ xmltodict.parse(response.text)
182
+ )
183
+ else:
184
+ # 如果是已解析的字典,直接查询
185
+ results = json_find_first(
186
+ "$.'soap:Envelope'.'soap:Body'.GetDataSetResponse.GetDataSetResult.'diffgr:diffgram'.NewDataSet.Table",
187
+ response
188
+ )
189
+
190
+ # 如果查询结果是单个字典(单行数据),转换为列表形式
191
+ if isinstance(results, dict):
192
+ return [results]
193
+
194
+ # 返回结果(可能是列表或 None)
195
+ return results
196
+
197
+
198
+ def build_actual_payment_items_query_sql(
199
+ columns: str = "",
200
+ conditions: str = "",
201
+ order_by: str = "order by cfi.ChargeFeeItemID"
202
+ ) -> str:
203
+ """
204
+ 生成查询实际支付项目的 SQL 语句
205
+
206
+ 该函数用于构建查询物业收费相关数据的 SQL 语句,支持自定义查询列、条件和排序方式。
207
+ 默认查询包含收费主列表、小区详情、收费项目、房间详情等多表关联数据。
208
+
209
+ 表关联关系说明:
210
+ - chargeMasterList (cml): 收费主列表(主表)
211
+ - EstateDetail (ed): 小区详情(通过 EstateID 关联)
212
+ - ChargeFeeItem (cfi): 收费项目(通过 ChargeMListID 关联)
213
+ - RoomDetail (rd): 房间详情(通过 RmId 关联)
214
+ - ChargeBillItem (cbi): 收费账单项目(通过 CBillItemID 关联)
215
+
216
+ Args:
217
+ columns: 额外的查询列(默认空字符串),会添加到默认列前面
218
+ conditions: 查询条件(默认空字符串),会拼接到 WHERE 子句后
219
+ order_by: 排序方式(默认 "order by cfi.ChargeFeeItemID")
220
+
221
+ Returns:
222
+ str: 完整的 SQL 查询语句
223
+
224
+ Note:
225
+ - 使用 LEFT JOIN 确保即使关联表无数据也能返回结果
226
+ - WHERE 子句使用 "1=1" 便于追加额外条件
227
+
228
+ Examples:
229
+ >>> build_actual_payment_items_query_sql(conditions="and cml.EstateID='E001'")
230
+ 'select cml.ChargeMListID,cml.ChargeMListNo,... from chargeMasterList as cml ... where 1=1 and cml.EstateID='E001' order by cfi.ChargeFeeItemID;'
231
+ """
232
+ # 定义默认查询列集合,包含收费相关的主要字段
233
+ default_columns = [
234
+ "cml.ChargeMListID", # 收费主列表ID
235
+ "cml.ChargeMListNo", # 收费单号
236
+ "cml.ChargeTime", # 收费时间
237
+ "cml.PayerName", # 付款人姓名
238
+ "cml.ChargePersonName", # 收费人姓名
239
+ "cml.ActualPayMoney", # 实际支付金额
240
+ "cml.EstateID", # 小区ID
241
+ "cml.ItemNames", # 项目名称
242
+ "ed.Caption as EstateName", # 小区名称(别名)
243
+ "cfi.ChargeFeeItemID", # 收费项目ID
244
+ "cfi.ActualAmount", # 实际金额
245
+ "cfi.SDate", # 开始日期
246
+ "cfi.EDate", # 结束日期
247
+ "cfi.RmId", # 房间ID
248
+ "rd.RmNo", # 房间号
249
+ "cml.CreateTime", # 创建时间
250
+ "cml.LastUpdateTime", # 最后更新时间
251
+ "cbi.ItemName", # 项目名称
252
+ "cbi.IsPayFull", # 是否已全额支付
253
+ "ABS(DATEDIFF(dd,cfi.EDate, cfi.SDate)) as PayDays", # 支付天数(计算字段)
254
+ ]
255
+
256
+ # 定义默认表连接关系,使用 LEFT JOIN 确保即使关联表无数据也能返回结果
257
+ table_joins = "".join([
258
+ " from chargeMasterList as cml", # 主表:收费主列表
259
+ " left join EstateDetail as ed on cml.EstateID=ed.EstateID", # 左联:小区详情
260
+ " left join ChargeFeeItem as cfi on cml.ChargeMListID=cfi.ChargeMListID", # 左联:收费项目
261
+ " left join RoomDetail as rd on cfi.RmId=rd.RmId", # 左联:房间详情
262
+ " left join ChargeBillItem as cbi on cfi.CBillItemID=cbi.CBillItemID", # 左联:收费账单项目
263
+ ])
264
+
265
+ # 构建并返回完整的 SQL 语句
266
+ # 使用 1=1 便于无条件时仍保持语法正确
267
+ return f"select {columns} {','.join(default_columns)} {table_joins} where 1=1 {conditions} {order_by};"
268
+
269
+
270
+ def payment_date_range_is_valid(
271
+ daily_fee: Union[int, float] = 0,
272
+ total_amount: Union[int, float] = 0,
273
+ start: str = "",
274
+ start_tz: Union[str, tzinfo] = "Asia/Shanghai",
275
+ end: str = "",
276
+ end_tz: Union[str, tzinfo] = "Asia/Shanghai",
277
+ ):
278
+ """
279
+ 验证支付金额与日期范围是否匹配
280
+
281
+ 根据每日费用、总金额和日期范围,验证总金额是否足够支付指定日期范围内的费用。
282
+ 主要用于校验物业费、租金等按日计费场景的金额是否合理。
283
+
284
+ 计算逻辑:
285
+ 1. 将每日费用和总金额转换为浮点数
286
+ 2. 如果总金额小于等于0,直接返回 False
287
+ 3. 使用 arrow 库解析开始日期和结束日期
288
+ 4. 计算日期范围内的总天数(包含起止两天)
289
+ 5. 验证总金额是否 >= 每日费用 × 总天数
290
+
291
+ Args:
292
+ daily_fee: 每日费用金额
293
+ total_amount: 实际支付的总金额
294
+ start: 计费开始日期字符串(支持多种格式)
295
+ start_tz: 开始日期的时区,默认为 "Asia/Shanghai"
296
+ end: 计费结束日期字符串(支持多种格式)
297
+ end_tz: 结束日期的时区,默认为 "Asia/Shanghai"
298
+
299
+ Returns:
300
+ bool: 如果总金额大于等于每日费用乘以天数则返回 True,否则返回 False
301
+
302
+ Note:
303
+ - 日期范围包含开始日期和结束日期两天
304
+ - 总金额小于等于0时直接返回 False
305
+ - 使用 arrow 库处理时区转换,避免时区问题导致的天数计算错误
306
+
307
+ Examples:
308
+ >>> payment_date_range_is_valid(daily_fee=10, total_amount=300, start="2024-01-01", end="2024-01-31")
309
+ True
310
+ >>> payment_date_range_is_valid(daily_fee=10, total_amount=290, start="2024-01-01", end="2024-01-31")
311
+ False
312
+ """
313
+ # 将输入转换为浮点数,确保数值计算准确性
314
+ daily_fee = float(daily_fee)
315
+ total_amount = float(total_amount)
316
+
317
+ # 总金额小于等于0时直接返回 False(无效金额)
318
+ if float(total_amount) <= 0:
319
+ return False
320
+
321
+ # 使用 arrow 库解析日期,支持多种日期格式和时区
322
+ start_arrow: arrow.Arrow = arrow.get(start, tzinfo=start_tz)
323
+ end_arrow: arrow.Arrow = arrow.get(end, tzinfo=end_tz)
324
+
325
+ # 计算日期范围内的总天数(包含起止两天)
326
+ # 使用 interval 方法生成每天的时间点,然后统计数量
327
+ total_days = sum(1 for _ in start_arrow.interval(
328
+ frame="days",
329
+ start=start_arrow.datetime,
330
+ end=end_arrow.datetime
331
+ ))
332
+
333
+ # 验证总金额是否足够支付指定天数的费用
334
+ return total_amount >= daily_fee * total_days
335
+
336
+
337
+ def filter_actual_payment_items(actual_payment_items: Optional[list[dict]] = None) -> list[dict]:
338
+ """
339
+ 过滤实际支付记录,移除配对记录
340
+
341
+ Args:
342
+ actual_payment_items: 每条记录包含 ChargeMListID, ActualAmount, SDate, EDate
343
+
344
+ Returns:
345
+ 移除配对记录后的列表
346
+ """
347
+ # 1. 按 (start_date, end_date, abs(amount)) 分组
348
+ actual_payment_items_groups = defaultdict(list)
349
+ for item in actual_payment_items:
350
+ sdate = item.get("SDate", "")
351
+ edate = item.get("EDate", "")
352
+ actual_amount = float(item.get("ActualAmount", 0))
353
+ group_key = (sdate, edate, abs(float(actual_amount)))
354
+ actual_payment_items_groups[group_key].append(item)
355
+ # return actual_payment_items_groups
356
+
357
+ # 2. 找出需要移除的 id
358
+ remove_ids = set()
359
+
360
+ for group_value in actual_payment_items_groups.values():
361
+ # 分离正金额(充值)和负金额(退款)
362
+ positives = [r for r in group_value if float(r.get("ActualAmount", 0)) > 0]
363
+ negatives = [r for r in group_value if float(r.get("ActualAmount", 0)) < 0]
364
+
365
+ # 按 id 排序(也可以按其他字段,比如 created_at)
366
+ positives.sort(key=lambda x: x['ChargeMListID'])
367
+ negatives.sort(key=lambda x: x['ChargeMListID'])
368
+
369
+ # 一一配对移除
370
+ paired_count = min(len(positives), len(negatives))
371
+ for i in range(paired_count):
372
+ remove_ids.add(positives[i]['ChargeMListID'])
373
+ remove_ids.add(negatives[i]['ChargeMListID'])
374
+
375
+ # 3. 返回未移除的记录
376
+ return [item for item in actual_payment_items if item['ChargeMListID'] not in remove_ids]
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-chanjet-toolkit
3
+ Version: 1.0.0
4
+ Summary: 畅捷通 T+ Python 客户端库,提供与畅捷通 T+ 智慧社区系统的 WebService 接口交互能力,支持同步和异步调用。
5
+ Author-email: Guolei <174000902@qq.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 郭磊
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://gitee.com/guolei19850528/py_chanjet_toolkit
29
+ Project-URL: Repository, https://gitee.com/guolei19850528/py_chanjet_toolkit.git
30
+ Project-URL: Issues, https://gitee.com/guolei19850528/py_chanjet_toolkit/issues
31
+ Project-URL: Documentation, https://gitee.com/guolei19850528/py_chanjet_toolkit/wikis
32
+ Keywords: chanjet,tplus,畅捷通,智慧社区,webservice,soap,api,client,async
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: Intended Audience :: System Administrators
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Operating System :: OS Independent
43
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
+ Classifier: Topic :: Internet :: WWW/HTTP
45
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
46
+ Classifier: Framework :: AsyncIO
47
+ Classifier: Typing :: Typed
48
+ Requires-Python: >=3.10
49
+ Description-Content-Type: text/markdown
50
+ License-File: LICENSE
51
+ Requires-Dist: pydantic>=2.0
52
+ Requires-Dist: jsonpath-ng>=1.5.3
53
+ Requires-Dist: jsonschema>=4.21.0
54
+ Requires-Dist: xmltodict>=1.0.4
55
+ Requires-Dist: arrow>=1.4.0
56
+ Requires-Dist: py-httpx-toolkit>=1.0.1
57
+ Dynamic: license-file
58
+
59
+ # py-chajet-toolkit
60
+
61
+ 畅捷通 T+ 系列产品的 Python 客户端库,提供与畅捷通 T+ 系统的 API 交互能力。
62
+
63
+ ## 功能特性
64
+
65
+ - 提供畅捷通 T+ 智慧社区 WebService 接口的同步和异步调用
66
+ - 封装 SOAP 请求的构建和响应解析
67
+ - 提供数据校验、日期处理、SQL 生成等辅助工具函数
68
+ - 支持 JSONPath 查询和 JSON Schema 校验
69
+
70
+ ## 安装方式
71
+
72
+ ### 使用 pip 安装
73
+
74
+ ```bash
75
+ pip install py-chajet-toolkit
76
+ ```
77
+
78
+ ### 使用 uv 安装
79
+
80
+ ```bash
81
+ uv add py-chajet-toolkit
82
+ ```
83
+
84
+ ## 快速开始
85
+
86
+ ### 基础使用示例
87
+
88
+ ```python
89
+ from py_chajet_toolkit.tplus.zkhb import ForcelandEstateService
90
+ from py_chajet_toolkit.tplus.zkhb.utils import (
91
+ build_get_data_set_post_xml,
92
+ build_get_data_set_results,
93
+ build_actual_payment_items_query_sql
94
+ )
95
+
96
+ # 初始化服务客户端
97
+ service = ForcelandEstateService(base_url="http://your-server/estate")
98
+
99
+ # 构建 SOAP 请求体
100
+ xml_data = build_get_data_set_post_xml({"sql": "SELECT * FROM EstateDetail"})
101
+
102
+ # 同步调用接口
103
+ response = service.get_data_set(content=xml_data)
104
+
105
+ # 解析响应数据
106
+ results = build_get_data_set_results(response)
107
+ print(results)
108
+ ```
109
+
110
+ ### 异步调用示例
111
+
112
+ ```python
113
+ import asyncio
114
+ from py_chajet_toolkit.tplus.zkhb import ForcelandEstateService
115
+ from py_chajet_toolkit.tplus.zkhb.utils import build_get_data_set_post_xml, build_get_data_set_results
116
+
117
+ async def main():
118
+ service = ForcelandEstateService(base_url="http://your-server/estate")
119
+ xml_data = build_get_data_set_post_xml({"sql": "SELECT * FROM ChargeMasterList"})
120
+
121
+ # 异步调用接口
122
+ response = await service.async_get_data_set(content=xml_data)
123
+ results = build_get_data_set_results(response)
124
+ print(results)
125
+
126
+ asyncio.run(main())
127
+ ```
128
+
129
+ ## API 说明
130
+
131
+ ### ForcelandEstateService
132
+
133
+ 畅捷通T+智慧社区服务客户端类,封装了与畅捷通T+智慧社区系统的WebService接口交互。
134
+
135
+ #### 初始化
136
+
137
+ ```python
138
+ service = ForcelandEstateService(
139
+ base_url="http://your-server/estate",
140
+ client_kwargs={"timeout": 120}
141
+ )
142
+ ```
143
+
144
+ #### 同步方法
145
+
146
+ - `get_data_set(client=None, client_kwargs=None, **kwargs)` - 调用 GetDataSet 接口
147
+
148
+ #### 异步方法
149
+
150
+ - `async_get_data_set(client=None, client_kwargs=None, **kwargs)` - 异步调用 GetDataSet 接口
151
+
152
+ ## Utils 工具函数
153
+
154
+ ### json_find_first
155
+
156
+ 使用 JSONPath 表达式从嵌套的 JSON 数据中查找第一个匹配项。
157
+
158
+ ```python
159
+ from py_chajet_toolkit.tplus.zkhb.utils import json_find_first
160
+
161
+ data = {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}
162
+ result = json_find_first("$.users[0].name", data)
163
+ # 返回: 'Alice'
164
+ ```
165
+
166
+ ### json_is_valid
167
+
168
+ 校验 JSON 数据是否符合指定的 JSON Schema 规范。
169
+
170
+ ```python
171
+ from py_chajet_toolkit.tplus.zkhb.utils import json_is_valid
172
+
173
+ schema = {
174
+ "type": "object",
175
+ "properties": {"id": {"type": "string"}},
176
+ "required": ["id"]
177
+ }
178
+ is_valid = json_is_valid(schema, {"id": "123"})
179
+ # 返回: True
180
+ ```
181
+
182
+ ### build_get_data_set_post_xml
183
+
184
+ 将请求数据转换为 SOAP GetDataSet 接口所需的 XML 格式。
185
+
186
+ ```python
187
+ from py_chajet_toolkit.tplus.zkhb.utils import build_get_data_set_post_xml
188
+
189
+ xml = build_get_data_set_post_xml({"sql": "SELECT * FROM EstateDetail"})
190
+ ```
191
+
192
+ ### build_get_data_set_results
193
+
194
+ 从 GetDataSet 接口响应中提取实际数据列表。
195
+
196
+ ```python
197
+ from py_chajet_toolkit.tplus.zkhb.utils import build_get_data_set_results
198
+
199
+ results = build_get_data_set_results(response)
200
+ ```
201
+
202
+ ### build_actual_payment_items_query_sql
203
+
204
+ 生成查询实际支付项目的 SQL 语句。
205
+
206
+ ```python
207
+ from py_chajet_toolkit.tplus.zkhb.utils import build_actual_payment_items_query_sql
208
+
209
+ sql = build_actual_payment_items_query_sql(
210
+ conditions="and cml.EstateID='E001'",
211
+ order_by="order by cml.CreateTime desc"
212
+ )
213
+ ```
214
+
215
+ ### payment_date_range_is_valid
216
+
217
+ 验证支付金额与日期范围是否匹配。
218
+
219
+ ```python
220
+ from py_chajet_toolkit.tplus.zkhb.utils import payment_date_range_is_valid
221
+
222
+ is_valid = payment_date_range_is_valid(
223
+ daily_fee=10,
224
+ total_amount=300,
225
+ start="2024-01-01",
226
+ end="2024-01-31"
227
+ )
228
+ # 返回: True
229
+ ```
230
+
231
+ ### filter_actual_payment_items
232
+
233
+ 过滤实际支付记录,移除配对的充值和退款记录。
234
+
235
+ ```python
236
+ from py_chajet_toolkit.tplus.zkhb.utils import filter_actual_payment_items
237
+
238
+ filtered_items = filter_actual_payment_items(actual_payment_items)
239
+ ```
240
+
241
+ ## 项目结构
242
+
243
+ ```
244
+ py_chajet_toolkit/
245
+ ├── tplus/
246
+ │ ├── zkhb/
247
+ │ │ ├── __init__.py # 智慧社区服务客户端类
248
+ │ │ ├── utils.py # 工具函数集合
249
+ │ │ └── responses.py # 响应处理模块
250
+ │ └── __init__.py
251
+ └── __init__.py
252
+ ```
253
+
254
+ ## 依赖
255
+
256
+ - arrow - 日期时间处理
257
+ - httpx - HTTP 客户端
258
+ - jsonpath_ng - JSONPath 查询
259
+ - jsonschema - JSON Schema 校验
260
+ - xmltodict - XML 与字典转换
261
+ - py_httpx_toolkit - HTTP 工具封装
262
+
263
+ ## 项目主页
264
+
265
+ [https://gitee.com/guolei19850528/py_chanjet_toolkit](https://gitee.com/guolei19850528/py_chanjet_toolkit)
266
+
267
+ ## 作者
268
+
269
+ **Author**: Lei Guo
270
+ **Email**: guolei@example.com
271
+
272
+ ## 许可证
273
+
274
+ MIT License
@@ -0,0 +1,10 @@
1
+ py_chajet_toolkit/__init__.py,sha256=xKia_U8WeX_VlXAZQd5ucHvKF82oSeBadiDTvN-wRyg,862
2
+ py_chajet_toolkit/tplus/__init__.py,sha256=9cHhA80MfeXYcf-Z_2F2qpNpAuXDqPl8rDYgkl9gDOg,445
3
+ py_chajet_toolkit/tplus/zkhb/__init__.py,sha256=n2dhiCurjJQtSPyKKV24_1uO7LMG29ASEkI93BWwP4c,5166
4
+ py_chajet_toolkit/tplus/zkhb/responses.py,sha256=oWlTUbGZHZHdXPwADq5g_QuW0NpFzaXNmskmXnPDils,2087
5
+ py_chajet_toolkit/tplus/zkhb/utils.py,sha256=Tq8rCF5wxOwktIQlMO5zyFRRcajcdMJEps7FQyjdbW0,15314
6
+ py_chanjet_toolkit-1.0.0.dist-info/licenses/LICENSE,sha256=HazFJNaQP3rbY1dvg3hAmO-FZ6v7DwM0YHwXvWsGG3A,1063
7
+ py_chanjet_toolkit-1.0.0.dist-info/METADATA,sha256=q3vChqme-EgRNsFFTKfXyOMIx-KYvVHN2uZFkgAFFL4,8011
8
+ py_chanjet_toolkit-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
9
+ py_chanjet_toolkit-1.0.0.dist-info/top_level.txt,sha256=OxwP24CYWVpzRu4AqrlD4dSVE-ocT3IBVlCm37ukAoI,18
10
+ py_chanjet_toolkit-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 郭磊
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 @@
1
+ py_chajet_toolkit