pixelarraylib 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,477 @@
1
+ import re
2
+ import traceback
3
+ import pandas as pd
4
+ from alibabacloud_bssopenapi20171214.client import Client as BssOpenApi20171214Client
5
+ from alibabacloud_tea_openapi import models as open_api_models
6
+ from alibabacloud_bssopenapi20171214 import models as bss_open_api_20171214_models
7
+ from alibabacloud_tea_util import models as util_models
8
+ from arraylib.monitor.feishu import Feishu
9
+
10
+ feishu_alert = Feishu("devtoolkit服务报警")
11
+
12
+
13
+ class BillingUtils:
14
+ def __init__(
15
+ self, access_key_id, access_key_secret, use_proxy=False, proxy_url=None
16
+ ):
17
+ """
18
+ description:
19
+ 初始化阿里云计费工具类
20
+ parameters:
21
+ access_key_id(str): 阿里云访问密钥ID
22
+ access_key_secret(str): 阿里云访问密钥Secret
23
+ use_proxy(bool): 是否使用代理,默认False
24
+ proxy_url(str): 代理URL,格式如 http://127.0.0.1:7897
25
+ """
26
+ config = open_api_models.Config(
27
+ access_key_id=access_key_id,
28
+ access_key_secret=access_key_secret,
29
+ endpoint="business.aliyuncs.com",
30
+ )
31
+
32
+ self.client = BssOpenApi20171214Client(config)
33
+
34
+ def _format_bill_response(self, response, with_comments=False):
35
+ """
36
+ description:
37
+ 格式化账单查询响应为JSON格式
38
+
39
+ parameters:
40
+ response(dict): API响应对象
41
+ with_comments(bool): 是否添加注释,默认False
42
+ return:
43
+ dict: 格式化后的响应数据
44
+ """
45
+ # 字段注释映射
46
+ field_comments = {
47
+ "Message": "错误信息",
48
+ "RequestId": "请求ID",
49
+ "BillingCycle": "账期",
50
+ "TotalCount": "总记录数",
51
+ "AccountID": "账号ID",
52
+ "AccountName": "账号名称",
53
+ "MaxResults": "本次请求所返回的最大记录数",
54
+ "NextToken": "用来表示当前调用返回读取到的位置,空代表数据已经读取完毕",
55
+ "Code": "状态码",
56
+ "Success": "是否成功",
57
+ "ProductName": "产品名称",
58
+ "SubOrderId": "该条账单对应的订单明细ID",
59
+ "BillAccountID": "账单所属账号ID",
60
+ "DeductedByCashCoupons": "代金券折扣",
61
+ "PaymentTime": "订单支付时间",
62
+ "PaymentAmount": "现金支付(包含信用额度退款抵扣)",
63
+ "DeductedByPrepaidCard": "储值卡抵扣",
64
+ "InvoiceDiscount": "优惠金额",
65
+ "UsageEndTime": "账单结束时间",
66
+ "Item": "账单类型",
67
+ "SubscriptionType": "订阅类型",
68
+ "PretaxGrossAmount": "原始金额",
69
+ "Currency": "货币类型",
70
+ "CommodityCode": "商品Code,与费用中心产品明细Code一致",
71
+ "UsageStartTime": "账单开始时间",
72
+ "AdjustAmount": "信用额度退款抵扣",
73
+ "Status": "支付状态",
74
+ "DeductedByCoupons": "优惠券抵扣",
75
+ "RoundDownDiscount": "抹零优惠",
76
+ "ProductDetail": "产品明细",
77
+ "ProductCode": "产品代码",
78
+ "ProductType": "产品类型",
79
+ "OutstandingAmount": "未结清金额",
80
+ "BizType": "业务类型",
81
+ "PipCode": "产品Code,与费用中心账单产品Code一致",
82
+ "PretaxAmount": "应付金额",
83
+ "OwnerID": "自帐号AccountID(多账号代付场景)",
84
+ "BillAccountName": "账单所属账号名称",
85
+ "RecordID": "订单号、账单号",
86
+ "CashAmount": "现金支付(不包含信用额度退款抵扣)",
87
+ }
88
+
89
+ def add_comment(value, field_name):
90
+ """为字段值添加注释"""
91
+ if with_comments and field_name in field_comments:
92
+ return f"{value} # {field_comments[field_name]}"
93
+ return value
94
+
95
+ try:
96
+ # 构建基础响应结构
97
+ formatted_response = {
98
+ "Message": add_comment(
99
+ "Successful!" if response.body.success else "Failed", "Message"
100
+ ),
101
+ "RequestId": add_comment(response.body.request_id, "RequestId"),
102
+ "Data": {
103
+ "BillingCycle": add_comment(
104
+ response.body.data.billing_cycle, "BillingCycle"
105
+ ),
106
+ "TotalCount": add_comment(
107
+ response.body.data.total_count, "TotalCount"
108
+ ),
109
+ "AccountID": add_comment(
110
+ getattr(response.body.data, "account_id", ""), "AccountID"
111
+ ),
112
+ "AccountName": add_comment(
113
+ getattr(response.body.data, "account_name", ""), "AccountName"
114
+ ),
115
+ "MaxResults": add_comment(
116
+ getattr(response.body.data, "max_results", 0), "MaxResults"
117
+ ),
118
+ "Items": {"Item": []},
119
+ },
120
+ "Code": add_comment(
121
+ "Success" if response.body.success else "Failed", "Code"
122
+ ),
123
+ "Success": add_comment(response.body.success, "Success"),
124
+ }
125
+
126
+ # 添加NextToken(如果存在)
127
+ if (
128
+ hasattr(response.body.data, "next_token")
129
+ and response.body.data.next_token
130
+ ):
131
+ formatted_response["Data"]["NextToken"] = add_comment(
132
+ response.body.data.next_token, "NextToken"
133
+ )
134
+
135
+ # 处理账单项
136
+ if hasattr(response.body.data, "items") and response.body.data.items:
137
+ items = response.body.data.items
138
+ if hasattr(items, "item") and items.item:
139
+ bill_items = items.item
140
+ if not isinstance(bill_items, list):
141
+ bill_items = [bill_items]
142
+
143
+ for item in bill_items:
144
+ item_data = {
145
+ "ProductName": add_comment(
146
+ getattr(item, "product_name", ""), "ProductName"
147
+ ),
148
+ "SubOrderId": add_comment(
149
+ getattr(item, "sub_order_id", ""), "SubOrderId"
150
+ ),
151
+ "BillAccountID": add_comment(
152
+ getattr(item, "bill_account_id", ""), "BillAccountID"
153
+ ),
154
+ "DeductedByCashCoupons": add_comment(
155
+ getattr(item, "deducted_by_cash_coupons", 0),
156
+ "DeductedByCashCoupons",
157
+ ),
158
+ "PaymentTime": add_comment(
159
+ getattr(item, "payment_time", ""), "PaymentTime"
160
+ ),
161
+ "PaymentAmount": add_comment(
162
+ getattr(item, "payment_amount", 0), "PaymentAmount"
163
+ ),
164
+ "DeductedByPrepaidCard": add_comment(
165
+ getattr(item, "deducted_by_prepaid_card", 0),
166
+ "DeductedByPrepaidCard",
167
+ ),
168
+ "InvoiceDiscount": add_comment(
169
+ getattr(item, "invoice_discount", 0), "InvoiceDiscount"
170
+ ),
171
+ "UsageEndTime": add_comment(
172
+ getattr(item, "usage_end_time", ""), "UsageEndTime"
173
+ ),
174
+ "Item": add_comment(getattr(item, "item", ""), "Item"),
175
+ "SubscriptionType": add_comment(
176
+ getattr(item, "subscription_type", ""),
177
+ "SubscriptionType",
178
+ ),
179
+ "PretaxGrossAmount": add_comment(
180
+ getattr(item, "pretax_gross_amount", 0),
181
+ "PretaxGrossAmount",
182
+ ),
183
+ "Currency": add_comment(
184
+ getattr(item, "currency", "CNY"), "Currency"
185
+ ),
186
+ "CommodityCode": add_comment(
187
+ getattr(item, "commodity_code", ""), "CommodityCode"
188
+ ),
189
+ "UsageStartTime": add_comment(
190
+ getattr(item, "usage_start_time", ""), "UsageStartTime"
191
+ ),
192
+ "AdjustAmount": add_comment(
193
+ getattr(item, "adjust_amount", 0), "AdjustAmount"
194
+ ),
195
+ "Status": add_comment(
196
+ getattr(item, "status", ""), "Status"
197
+ ),
198
+ "DeductedByCoupons": add_comment(
199
+ getattr(item, "deducted_by_coupons", 0),
200
+ "DeductedByCoupons",
201
+ ),
202
+ "RoundDownDiscount": add_comment(
203
+ getattr(item, "round_down_discount", 0),
204
+ "RoundDownDiscount",
205
+ ),
206
+ "ProductDetail": add_comment(
207
+ getattr(item, "product_detail", ""), "ProductDetail"
208
+ ),
209
+ "ProductCode": add_comment(
210
+ getattr(item, "product_code", ""), "ProductCode"
211
+ ),
212
+ "ProductType": add_comment(
213
+ getattr(item, "product_type", ""), "ProductType"
214
+ ),
215
+ "OutstandingAmount": add_comment(
216
+ getattr(item, "outstanding_amount", 0),
217
+ "OutstandingAmount",
218
+ ),
219
+ "BizType": add_comment(
220
+ getattr(item, "biz_type", ""), "BizType"
221
+ ),
222
+ "PipCode": add_comment(
223
+ getattr(item, "pip_code", ""), "PipCode"
224
+ ),
225
+ "PretaxAmount": add_comment(
226
+ getattr(item, "pretax_amount", 0), "PretaxAmount"
227
+ ),
228
+ "OwnerID": add_comment(
229
+ getattr(item, "owner_id", ""), "OwnerID"
230
+ ),
231
+ "BillAccountName": add_comment(
232
+ getattr(item, "bill_account_name", ""),
233
+ "BillAccountName",
234
+ ),
235
+ "RecordID": add_comment(
236
+ getattr(item, "record_id", ""), "RecordID"
237
+ ),
238
+ "CashAmount": add_comment(
239
+ getattr(item, "cash_amount", 0), "CashAmount"
240
+ ),
241
+ }
242
+ formatted_response["Data"]["Items"]["Item"].append(item_data)
243
+
244
+ return formatted_response
245
+
246
+ except Exception as e:
247
+ feishu_alert.send(traceback.format_exc())
248
+ error_response = {
249
+ "Message": add_comment(f"Format error: {str(e)}", "Message"),
250
+ "RequestId": add_comment(
251
+ getattr(response.body, "request_id", ""), "RequestId"
252
+ ),
253
+ "Data": {},
254
+ "Code": add_comment("Error", "Code"),
255
+ "Success": add_comment(False, "Success"),
256
+ }
257
+ return error_response
258
+
259
+ def _query_bill_once(
260
+ self,
261
+ billing_cycle: str,
262
+ max_results: int = 20,
263
+ next_token: str = None,
264
+ product_code: str = None,
265
+ subscription_type: str = None,
266
+ product_type: str = None,
267
+ is_hide_zero_charge: bool = False,
268
+ ) -> dict:
269
+ """
270
+ description:
271
+ 查询阿里云账单信息
272
+ parameters:
273
+ billing_cycle: 账单周期,格式:YYYY-MM,默认:2025-08
274
+ max_results: 最大返回记录数,默认:20
275
+ next_token: 下一页的token,用于分页查询
276
+ product_code: 产品代码,可选
277
+ subscription_type: 订阅类型,可选值:Subscription(预付费)、PayAsYouGo(后付费)
278
+ product_type: 产品类型,可选
279
+ is_hide_zero_charge: 是否隐藏零费用记录,默认:False
280
+ return:
281
+ dict: 格式化后的响应数据
282
+ 返回格式化的JSON响应,包含以下字段:
283
+ - Message: 错误信息
284
+ - RequestId: 请求ID
285
+ - Data: 返回数据
286
+ - BillingCycle: 账期
287
+ - TotalCount: 总记录数
288
+ - AccountID: 账号ID
289
+ - AccountName: 账号名称
290
+ - MaxResults: 本次请求所返回的最大记录数
291
+ - NextToken: 用来表示当前调用返回读取到的位置,空代表数据已经读取完毕
292
+ - Items: 账单详情列表
293
+ - Item: 账单详情数组,包含产品名称、费用信息、支付状态等详细字段
294
+ """
295
+ # 构建请求参数
296
+ request_params = {
297
+ "billing_cycle": billing_cycle,
298
+ "max_results": max_results,
299
+ "is_hide_zero_charge": is_hide_zero_charge,
300
+ }
301
+
302
+ # 添加可选参数
303
+ if next_token:
304
+ request_params["next_token"] = next_token
305
+ if product_code:
306
+ request_params["product_code"] = product_code
307
+ if subscription_type:
308
+ request_params["subscription_type"] = subscription_type
309
+ if product_type:
310
+ request_params["product_type"] = product_type
311
+
312
+ query_settle_bill_request = bss_open_api_20171214_models.QuerySettleBillRequest(
313
+ **request_params
314
+ )
315
+ runtime = util_models.RuntimeOptions()
316
+ try:
317
+ # 调用 API 并获取返回结果
318
+ response = self.client.query_settle_bill_with_options(
319
+ query_settle_bill_request, runtime
320
+ )
321
+
322
+ # 格式化响应并返回带注释的JSON格式
323
+ formatted_response = self._format_bill_response(
324
+ response, with_comments=True
325
+ )
326
+ return formatted_response
327
+
328
+ except Exception as error:
329
+ feishu_alert.send(traceback.format_exc())
330
+ error_msg = getattr(error, "message", str(error))
331
+ # 诊断地址
332
+ diagnose_url = ""
333
+ if hasattr(error, "data") and error.data:
334
+ diagnose_url = error.data.get("Recommend", "")
335
+
336
+ # 返回错误响应
337
+ error_response = {
338
+ "Message": f"错误信息: {error_msg}",
339
+ "RequestId": "",
340
+ "Data": {},
341
+ "Code": "Error",
342
+ "Success": False,
343
+ "DiagnoseUrl": diagnose_url,
344
+ }
345
+ return error_response
346
+
347
+ def query_bill(self, billing_cycle: str, batch_size: int = 300):
348
+ """
349
+ description:
350
+ 按月份查询阿里云账单信息,会自动分页查询
351
+ parameters:
352
+ billing_cycle: 账单周期,格式:YYYY-MM,默认:2025-08
353
+ batch_size: 每页返回的记录数,默认:300
354
+ return:
355
+ dict: 格式化后的响应数据
356
+ """
357
+ response = self._query_bill_once(billing_cycle=billing_cycle)
358
+ bill_data = response.get("Data", {}).get("Items", {}).get("Item", [])
359
+ next_token = response.get("Data", {}).get("NextToken")
360
+ while next_token:
361
+ response = self._query_bill_once(
362
+ billing_cycle=billing_cycle,
363
+ next_token=next_token,
364
+ max_results=batch_size,
365
+ )
366
+ bill_data.extend(response.get("Data", {}).get("Items", {}).get("Item", []))
367
+ next_token = response.get("Data", {}).get("NextToken")
368
+ return bill_data
369
+
370
+ def save_bill(
371
+ self,
372
+ bill_data: dict,
373
+ output_path: str,
374
+ file_format: str = "csv",
375
+ translate_headers: bool = False,
376
+ ) -> str:
377
+ """
378
+ description:
379
+ 将query_bill的输出结果保存为表格文件
380
+ parameters:
381
+ bill_data: query_bill方法返回的账单数据
382
+ output_path: 输出文件路径
383
+ file_format: 文件格式,支持 'excel', 'csv', 'json',默认 'excel'
384
+ translate_headers: 是否将表头翻译为中文,默认 False
385
+ return:
386
+ str: 保存的文件路径
387
+ """
388
+ try:
389
+ # 内部工具:清洗字符串
390
+ # 1) 去除注释(例如 "value # 注释" -> "value")
391
+ # 2) 去除首尾空白,并将内部连续空白压缩为一个空格
392
+ # 3) 去除常见的不可见空白字符(如不间断空格、零宽空格)
393
+ def _clean_string(value):
394
+ if not isinstance(value, str):
395
+ return value
396
+ # 去注释
397
+ if " # " in value:
398
+ value = value.split(" # ", 1)[0]
399
+ # 替换不可见空白
400
+ value = (
401
+ value.replace("\u00a0", " ")
402
+ .replace("\u200b", "")
403
+ .replace("\u200c", "")
404
+ .replace("\u200d", "")
405
+ )
406
+ # 规范化空白:先strip,再将内部所有空白替换为下划线
407
+ value = value.strip()
408
+ if value:
409
+ # 将任意连续空白缩为一个空格
410
+ # 1) 将任意连续空白(空格、制表符等)替换为单个下划线
411
+ value = re.sub(r"\s+", "_", value)
412
+ return value
413
+
414
+ # 创建DataFrame
415
+ df = pd.DataFrame(bill_data)
416
+ # 清洗字符串
417
+ df = df.applymap(_clean_string)
418
+ # 将NaN替换为空字符串,确保表格为空显示空,不是NaN
419
+ df = df.fillna("")
420
+
421
+ # 可选:根据注释映射翻译表头为中文
422
+ if translate_headers:
423
+ header_map = {
424
+ # 公共字段
425
+ "BillingCycle": "账期",
426
+ "AccountID": "账号ID",
427
+ "AccountName": "账号名称",
428
+ # 账单项字段(与 _format_bill_response 中注释一致)
429
+ "ProductName": "产品名称",
430
+ "SubOrderId": "订单明细ID",
431
+ "BillAccountID": "账单所属账号ID",
432
+ "DeductedByCashCoupons": "代金券折扣",
433
+ "PaymentTime": "订单支付时间",
434
+ "PaymentAmount": "现金支付(含信用额度退款抵扣)",
435
+ "DeductedByPrepaidCard": "储值卡抵扣",
436
+ "InvoiceDiscount": "优惠金额",
437
+ "UsageEndTime": "账单结束时间",
438
+ "Item": "账单类型",
439
+ "SubscriptionType": "订阅类型",
440
+ "PretaxGrossAmount": "原始金额",
441
+ "Currency": "货币类型",
442
+ "CommodityCode": "商品Code",
443
+ "UsageStartTime": "账单开始时间",
444
+ "AdjustAmount": "信用额度退款抵扣",
445
+ "Status": "支付状态",
446
+ "DeductedByCoupons": "优惠券抵扣",
447
+ "RoundDownDiscount": "抹零优惠",
448
+ "ProductDetail": "产品明细",
449
+ "ProductCode": "产品代码",
450
+ "ProductType": "产品类型",
451
+ "OutstandingAmount": "未结清金额",
452
+ "BizType": "业务类型",
453
+ "PipCode": "产品Code",
454
+ "PretaxAmount": "应付金额",
455
+ "OwnerID": "自帐号AccountID",
456
+ "BillAccountName": "账单所属账号名称",
457
+ "RecordID": "订单号/账单号",
458
+ "CashAmount": "现金支付(不含信用额度退款抵扣)",
459
+ }
460
+ # 仅重命名存在的列
461
+ df = df.rename(
462
+ columns={k: v for k, v in header_map.items() if k in df.columns}
463
+ )
464
+
465
+ # 保存文件
466
+ if file_format == "excel":
467
+ df.to_excel(output_path, index=False, engine="openpyxl")
468
+ elif file_format == "csv":
469
+ df.to_csv(output_path, index=False, encoding="utf-8-sig")
470
+ elif file_format == "json":
471
+ df.to_json(output_path, orient="records", force_ascii=False, indent=2)
472
+
473
+ return output_path
474
+
475
+ except Exception as e:
476
+ feishu_alert.send(traceback.format_exc())
477
+ raise Exception(f"保存账单数据失败: {str(e)}")