pytbox 0.2.6__py3-none-any.whl → 0.2.9__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.
@@ -168,7 +168,8 @@ class VictoriaMetrics:
168
168
  ReturnResponse:
169
169
  code = 0 正常, code = 1 异常, code = 2 没有查询到数据, 建议将其判断为正常
170
170
  '''
171
- query = f'avg_over_time((ping_result_code{{target="{target}"}})[{last_minute}m])'
171
+ query = f'min_over_time(ping_result_code{{target="{target}"}}[{last_minute}m])'
172
+ # query = f'avg_over_time((ping_result_code{{target="{target}"}})[{last_minute}m])'
172
173
  if self.env == 'dev':
173
174
  r = load_dev_file(dev_file)
174
175
  else:
pytbox/network/meraki.py CHANGED
@@ -4,16 +4,21 @@ from typing import Any, Literal
4
4
  import requests
5
5
  from ..utils.response import ReturnResponse
6
6
 
7
+
7
8
  class Meraki:
8
9
  '''
9
10
  Meraki Client
10
11
  '''
11
- def __init__(self, api_key: str=None, organization_id: str=None, timeout: int=10):
12
+ def __init__(self, api_key: str=None, organization_id: str=None, timeout: int=10, region: Literal['global', 'china']='china'):
12
13
  if not api_key:
13
14
  raise ValueError("api_key is required")
14
15
  if not organization_id:
15
16
  raise ValueError("organization_id is required")
16
- self.base_url = 'https://api.meraki.cn/api/v1'
17
+ if region == 'china':
18
+ self.base_url = 'https://api.meraki.cn/api/v1'
19
+ else:
20
+ self.base_url = 'https://api.meraki.com/api/v1'
21
+
17
22
  self.headers = {
18
23
  "Authorization": f"Bearer {api_key}",
19
24
  "Accept": "application/json"
@@ -21,6 +26,34 @@ class Meraki:
21
26
  self.organization_id = organization_id
22
27
  self.timeout = timeout
23
28
 
29
+ def get_organizations(self) -> ReturnResponse:
30
+ '''
31
+ https://developer.cisco.com/meraki/api-v1/get-organizations/
32
+ '''
33
+ r = requests.get(
34
+ f"{self.base_url}/organizations",
35
+ headers=self.headers,
36
+ timeout=self.timeout
37
+ )
38
+ if r.status_code == 200:
39
+ return ReturnResponse(code=0, msg=f"获取组织成功", data=r.json())
40
+ return ReturnResponse(code=1, msg=f"获取组织失败: {r.status_code} {r.text}")
41
+
42
+ def get_api_requests(self, timespan: int=5*60) -> ReturnResponse:
43
+
44
+ params = {}
45
+ params['timespan'] = timespan
46
+
47
+ r = requests.get(
48
+ url=f"{self.base_url}/organizations/{self.organization_id}/apiRequests",
49
+ headers=self.headers,
50
+ params=params,
51
+ timeout=self.timeout
52
+ )
53
+ if r.status_code == 200:
54
+ return ReturnResponse(code=0, msg='获取 API 请求数量成功', data=r.json())
55
+ return ReturnResponse(code=1, msg=f"获取 API 请求失败: {r.status_code} - {r.text}", data=None)
56
+
24
57
  def get_networks(self, tags: list[str]=None) -> ReturnResponse:
25
58
  '''
26
59
  https://developer.cisco.com/meraki/api-v1/get-organization-networks/
@@ -146,24 +179,347 @@ class Meraki:
146
179
  return ReturnResponse(code=3, msg=f"设备 {serial} 还未添加过", data=None)
147
180
  return ReturnResponse(code=1, msg=f"获取设备详情失败: {r.status_code} - {r.text}", data=None)
148
181
 
149
- def get_device_availabilities(self, network_id: str=None,
182
+ def get_device_availability(self, network_id: list=None,
150
183
  status: Literal['online', 'offline', 'dormant', 'alerting']=None,
151
- serial: str=None) -> ReturnResponse:
184
+ serial: str=None,
185
+ tags: list=None,
186
+ get_all: bool=False) -> ReturnResponse:
187
+ '''
188
+ https://developer.cisco.com/meraki/api-v1/get-organization-devices-availabilities/
189
+
190
+ Args:
191
+ network_id (str, optional): 如果是列表, 不能传太多 network_id
192
+ status (Literal['online', 'offline', 'dormant', 'alerting'], optional): _description_. Defaults to None.
193
+ serial (str, optional): _description_. Defaults to None.
194
+ get_all (bool, optional): _description_. Defaults to False.
195
+
196
+ Returns:
197
+ ReturnResponse: _description_
198
+ '''
152
199
  params = {}
153
200
 
154
201
  if status:
155
202
  params["statuses[]"] = status
156
203
 
157
204
  if serial:
158
- params["serials[]"] = serial
205
+ if isinstance(serial, str):
206
+ params["serials[]"] = [serial]
207
+ else:
208
+ params["serials[]"] = serial
209
+
210
+ if network_id:
211
+ if isinstance(network_id, str):
212
+ params["networkIds[]"] = [network_id]
213
+ else:
214
+ params["networkIds[]"] = network_id
215
+
216
+ if tags:
217
+ params["tags[]"] = tags
218
+
219
+ # 如果需要获取所有数据,设置每页最大数量
220
+ if get_all:
221
+ params['perPage'] = 1000
222
+
223
+ all_data = []
224
+ url = f"{self.base_url}/organizations/{self.organization_id}/devices/availabilities"
159
225
 
226
+ while url:
227
+ r = requests.get(
228
+ url=url,
229
+ headers=self.headers,
230
+ params=params if url == f"{self.base_url}/organizations/{self.organization_id}/devices/availabilities" else {},
231
+ timeout=self.timeout
232
+ )
233
+
234
+ if r.status_code != 200:
235
+ return ReturnResponse(code=1, msg=f"获取设备健康状态失败: {r.status_code} - {r.text}", data=None)
236
+
237
+ data = r.json()
238
+ all_data.extend(data)
239
+
240
+ # 如果不需要获取所有数据,只返回第一页
241
+ if not get_all:
242
+ return ReturnResponse(code=0, msg=f"获取设备健康状态成功,共 {len(data)} 条", data=data)
243
+
244
+ # 解析 Link header 获取下一页 URL
245
+ url = None
246
+ link_header = r.headers.get('Link', '')
247
+ if link_header:
248
+ # 解析 Link header,格式如: '<url>; rel=next, <url>; rel=prev'
249
+ for link in link_header.split(','):
250
+ link = link.strip()
251
+ if 'rel=next' in link or 'rel="next"' in link:
252
+ # 提取 URL (在 < > 之间)
253
+ url = link.split(';')[0].strip('<> ')
254
+ break
255
+
256
+ return ReturnResponse(code=0, msg=f"获取设备健康状态成功,共 {len(all_data)} 条", data=all_data)
257
+
258
+
259
+ def get_device_availabilities_change_history(self, network_id: str=None, serial: str=None) -> ReturnResponse:
260
+ '''
261
+ https://developer.cisco.com/meraki/api-v1/get-organization-devices-availabilities/
262
+
263
+ Args:
264
+ network_id (str, optional): _description_. Defaults to None.
265
+ serial (str, optional): _description_. Defaults to None.
266
+
267
+ Returns:
268
+ ReturnResponse: _description_
269
+ '''
270
+ params = {}
160
271
  if network_id:
161
- params["networkIds[]"] = network_id
272
+ params['networkId'] = network_id
273
+ if serial:
274
+ params['serial'] = serial
162
275
 
163
276
  r = requests.get(
164
- url=f"{self.base_url}/organizations/{self.organization_id}/devices/availabilities",
277
+ url=f"{self.base_url}/organizations/{self.organization_id}/devices/availabilities/changeHistory",
165
278
  headers=self.headers,
166
279
  params=params,
167
280
  timeout=self.timeout
168
281
  )
169
- return ReturnResponse(code=0, msg=f"获取设备健康状态成功", data=r.json())
282
+ if r.status_code == 200:
283
+ return ReturnResponse(code=0, msg=f"获取设备健康状态变化历史成功", data=r.json())
284
+ return ReturnResponse(code=1, msg=f"获取设备健康状态变化历史失败: {r.status_code} - {r.text}", data=None)
285
+
286
+ def reboot_device(self, serial: str) -> ReturnResponse:
287
+ '''
288
+ 该接口 60s 只能执行一次
289
+
290
+ Args:
291
+ serial (str): _description_
292
+
293
+ Returns:
294
+ ReturnResponse: _description_
295
+ '''
296
+ r = requests.post(
297
+ url=f"{self.base_url}/devices/{serial}/reboot",
298
+ headers=self.headers,
299
+ timeout=self.timeout
300
+ )
301
+ if r.status_code == 202 and r.json()['success'] == True:
302
+ return ReturnResponse(code=0, msg=f"重启 {serial} 成功", data=r.json())
303
+
304
+ try:
305
+ error_msg = r.json()['error']
306
+ except KeyError:
307
+ error_msg = r.json()
308
+ return ReturnResponse(code=1, msg=f"重启 {serial} 失败, 报错 {error_msg}", data=None)
309
+
310
+ def get_alerts(self):
311
+ # from datetime import datetime, timedelta
312
+ params = {}
313
+ params['tsStart'] = "2025-10-20T00:00:00Z"
314
+ params['tsEnd'] = "2025-10-30T00:00:00Z"
315
+ # # 获取昨天0:00的时间戳(秒)
316
+ # yesterday = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=1)
317
+ # ts_start = int(yesterday.timestamp()) * 1000
318
+ # params['tsStart'] = str(ts_start)
319
+ # print(params)
320
+ r = requests.get(
321
+ url=f"{self.base_url}/organizations/{self.organization_id}/assurance/alerts",
322
+ headers=self.headers,
323
+ timeout=self.timeout,
324
+ params=params
325
+ )
326
+ for i in r.json():
327
+ print(i)
328
+ # return ReturnResponse(code=0, msg="获取告警成功", data=r.json())
329
+
330
+ def get_network_events(self, network_id):
331
+ params = {}
332
+ params['productType'] = "wireless"
333
+
334
+ print(params)
335
+ r = requests.get(
336
+ url=f"{self.base_url}/networks/{network_id}/events",
337
+ headers=self.headers,
338
+ timeout=self.timeout,
339
+ params=params
340
+ )
341
+ if r.status_code == 200:
342
+ return ReturnResponse(code=0, msg=f"获取网络事件成功", data=r.json())
343
+ return ReturnResponse(code=1, msg=f"获取网络事件失败: {r.status_code} - {r.text}", data=None)
344
+
345
+ def get_wireless_failcounter(self, network_id: str, timespan: int=5*60, serial: str=None):
346
+ '''
347
+ https://developer.cisco.com/meraki/api-v1/get-network-wireless-failed-connections/
348
+ '''
349
+ params = {}
350
+ params['timespan'] = timespan
351
+ if serial:
352
+ params['serial'] = serial
353
+
354
+ r = requests.get(
355
+ url=f"{self.base_url}/networks/{network_id}/wireless/failedConnections",
356
+ headers=self.headers,
357
+ timeout=self.timeout,
358
+ params=params
359
+ )
360
+ if r.status_code == 200:
361
+ return ReturnResponse(code=0, msg=f"获取无线失败连接成功", data=r.json())
362
+ return ReturnResponse(code=1, msg=f"获取无线失败连接失败: {r.status_code} - {r.text}", data=None)
363
+
364
+ def claim_network_devices(self, network_id: str, serials: list[str]) -> ReturnResponse:
365
+ '''
366
+ https://developer.cisco.com/meraki/api-v1/claim-network-devices/
367
+
368
+ Args:
369
+ network_id (_type_): _description_
370
+ serials (list): _description_
371
+
372
+ Returns:
373
+ ReturnResponse: _description_
374
+ '''
375
+ new_serials = []
376
+ already_claimed_serials = []
377
+
378
+ for serial in serials:
379
+ r = self.get_device_detail(serial=serial)
380
+ if r.code == 0:
381
+ already_claimed_serials.append(serial)
382
+ elif r.code == 3:
383
+ new_serials.append(serial)
384
+ else:
385
+ new_serials.append(serial)
386
+
387
+ body = {
388
+ "serials": new_serials,
389
+ "addAtomically": True
390
+ }
391
+
392
+ r = requests.post(
393
+ url=f"{self.base_url}/networks/{network_id}/devices/claim",
394
+ headers=self.headers,
395
+ json=body,
396
+ timeout=self.timeout + 10
397
+ )
398
+
399
+ if len(already_claimed_serials) == len(serials):
400
+ code = 0
401
+ msg = f"All {len(already_claimed_serials)} devices are already claimed"
402
+ elif len(already_claimed_serials) > 0:
403
+ code = 0
404
+ msg = f"Some {len(already_claimed_serials)} devices are already claimed"
405
+ else:
406
+ code = 0
407
+ msg = f"Claim network devices successfully, claimed {len(new_serials)} devices"
408
+
409
+ return ReturnResponse(code=code, msg=msg)
410
+
411
+ def update_device(self, serial: str, name: str=None, tags: list=None, address: str=None, lat: float=None, lng: float=None) -> ReturnResponse:
412
+ '''
413
+ https://developer.cisco.com/meraki/api-v1/update-device/
414
+ '''
415
+ body = {}
416
+ if name:
417
+ body['name'] = name
418
+ if tags:
419
+ body['tags'] = tags
420
+ if address:
421
+ body['address'] = address
422
+ if lat:
423
+ body['lat'] = lat
424
+ if lng:
425
+ body['lng'] = lng
426
+ r = requests.put(
427
+ url=f"{self.base_url}/devices/{serial}",
428
+ headers=self.headers,
429
+ json=body,
430
+ timeout=self.timeout
431
+ )
432
+
433
+
434
+ def update_device(self,
435
+ config_template_id: str=None,
436
+ serial: str=None,
437
+ name: str=None,
438
+ tags: list=None,
439
+ address: str=None,
440
+ lat: float=None,
441
+ lng: float=None,
442
+ switch_profile_id: str=None
443
+ ) -> ReturnResponse:
444
+
445
+ body = {
446
+ "name": name,
447
+ "tags": tags,
448
+ }
449
+
450
+ if address:
451
+ body['address'] = address
452
+ body["moveMapMarker"] = True
453
+
454
+ if lat:
455
+ body['Lat'] = lat
456
+
457
+ if lng:
458
+ body['Lng'] = lng
459
+
460
+ if not switch_profile_id:
461
+ model = self.get_device_detail(serial=serial).data.get('model')
462
+ for switch_profile in self.get_switch_profiles(config_template_id=config_template_id).data:
463
+ if switch_profile.get('model') == model:
464
+ switch_profile_id = switch_profile.get('switchProfileId')
465
+ body['switchProfileId'] = switch_profile_id
466
+ else:
467
+ body['switchProfileId'] = switch_profile_id
468
+
469
+ response = requests.put(
470
+ url=f"{self.base_url}/devices/{serial}",
471
+ headers=self.headers,
472
+ json=body,
473
+ timeout=3
474
+ )
475
+ if response.status_code == 200:
476
+ return ReturnResponse(code=0, msg=f"更新设备 {serial} 成功", data=response.json())
477
+ else:
478
+ return ReturnResponse(code=1, msg=f"更新设备 {serial} 失败: {response.status_code} - {response.text}", data=None)
479
+
480
+ def get_switch_ports(self, serial: str) -> ReturnResponse:
481
+ '''
482
+ https://developer.cisco.com/meraki/api-v1/get-device-switch-ports/
483
+ '''
484
+ r = requests.get(
485
+ url=f"{self.base_url}/devices/{serial}/switch/ports/statuses",
486
+ headers=self.headers,
487
+ timeout=self.timeout
488
+ )
489
+ if r.status_code == 200:
490
+ return ReturnResponse(code=0, msg=f"获取交换机端口状态成功", data=r.json())
491
+ return ReturnResponse(code=1, msg=f"获取交换机端口状态失败: {r.status_code} - {r.text}", data=None)
492
+
493
+ def get_ssids(self, network_id):
494
+ '''
495
+ https://developer.cisco.com/meraki/api-v1/get-network-wireless-ssids/
496
+ '''
497
+ r = requests.get(
498
+ url=f"{self.base_url}/networks/{network_id}/wireless/ssids",
499
+ headers=self.headers,
500
+ timeout=self.timeout
501
+ )
502
+ if r.status_code == 200:
503
+ return ReturnResponse(code=0, msg=f"获取 SSID 成功", data=r.json())
504
+ return ReturnResponse(code=1, msg=f"获取 SSID 失败: {r.status_code} - {r.text}", data=None)
505
+
506
+ def update_ssid(self, network_id, ssid_number, body):
507
+ '''
508
+ https://developer.cisco.com/meraki/api-v1/update-network-wireless-ssid/
509
+
510
+ Args:
511
+ network_id (_type_): _description_
512
+ ssid_number (_type_): _description_
513
+
514
+ Returns:
515
+ _type_: _description_
516
+ '''
517
+ r = requests.put(
518
+ url=f"{self.base_url}/networks/{network_id}/wireless/ssids/{ssid_number}",
519
+ headers=self.headers,
520
+ timeout=self.timeout,
521
+ json=body
522
+ )
523
+ if r.status_code == 200:
524
+ return ReturnResponse(code=0, msg=f"更新 SSID 成功", data=r.json())
525
+ return ReturnResponse(code=1, msg=f"更新 SSID 失败: {r.status_code} - {r.text}", data=None)
pytbox/pyjira.py CHANGED
@@ -314,13 +314,13 @@ class PyJira:
314
314
  else:
315
315
  return ReturnResponse(code=1, msg=f'添加评论 [{comment}] 失败, 返回值: {r.text}', data=r.json())
316
316
 
317
- def issue_search(self, jql: str, max_results: int = 50, fields: Optional[List[str]] = None) -> ReturnResponse:
318
- """使用JQL搜索JIRA任务
317
+ def issue_search(self, jql: str, max_results: int = 1000, fields: Optional[List[str]] = None) -> ReturnResponse:
318
+ """使用JQL搜索JIRA任务(支持自动分页获取所有结果)
319
319
  https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-get
320
320
 
321
321
  Args:
322
322
  jql: JQL查询字符串
323
- max_results: 最大返回结果数,默认50
323
+ max_results: 最大返回结果数,默认1000。会自动分页获取
324
324
  fields: 需要返回的字段列表,默认返回所有字段
325
325
 
326
326
  Returns:
@@ -328,25 +328,72 @@ class PyJira:
328
328
  """
329
329
  url = f"{self.base_url}/rest/api/3/search/jql"
330
330
 
331
- # 构建查询参数
332
- params = {
333
- "jql": jql,
334
- "maxResults": max_results,
335
- "startAt": 0
336
- }
331
+ # Jira API 单次请求最多返回100条,需要分页
332
+ page_size = 100
333
+ all_issues = []
334
+ seen_keys = set() # 用于去重
335
+ next_page_token = None
336
+ page_count = 0
337
337
 
338
- # 如果指定了字段,添加到查询参数中
339
- if isinstance(fields, list):
340
- params["fields"] = ",".join(fields)
341
- else:
342
- params["fields"] = fields
338
+ while len(all_issues) < max_results:
339
+ page_count += 1
340
+
341
+ # 构建查询参数
342
+ params = {
343
+ "jql": jql,
344
+ "maxResults": page_size
345
+ }
346
+
347
+ # 如果有下一页的 token,添加到参数中(按文档使用 nextPageToken 参数名)
348
+ if next_page_token:
349
+ params["nextPageToken"] = next_page_token
350
+
351
+ # 如果指定了字段,添加到查询参数中
352
+ if isinstance(fields, list):
353
+ params["fields"] = ",".join(fields)
354
+ else:
355
+ params["fields"] = fields
356
+
357
+ r = self.session.get(url, headers=self.headers, params=params, timeout=self.timeout)
358
+
359
+ if r.status_code != 200:
360
+ return ReturnResponse(code=1, msg=f'获取 issue 失败, status code: {r.status_code}, 报错: {r.text}')
361
+
362
+ data = r.json()
363
+ issues = data.get('issues', [])
364
+
365
+ if not issues:
366
+ break
367
+
368
+ # 去重:只添加未见过的 issue
369
+ new_issues_count = 0
370
+ for issue in issues:
371
+ issue_key = issue.get('key')
372
+ if issue_key and issue_key not in seen_keys:
373
+ all_issues.append(issue)
374
+ seen_keys.add(issue_key)
375
+ new_issues_count += 1
376
+
377
+ # 检查是否是最后一页
378
+ is_last = data.get('isLast', True)
379
+ next_page_token = data.get('nextPageToken')
380
+
381
+ # 如果是最后一页或没有新数据,退出循环
382
+ if is_last or new_issues_count == 0:
383
+ break
343
384
 
344
- r = self.session.get(url, headers=self.headers, params=params, timeout=self.timeout)
385
+ # 如果获取的数据超过 max_results,截断到指定数量
386
+ if len(all_issues) > max_results:
387
+ all_issues = all_issues[:max_results]
345
388
 
346
- if r.status_code == 200:
347
- return ReturnResponse(code=0, msg='', data=r.json())
348
- else:
349
- return ReturnResponse(code=1, msg=f'获取 issue 失败, status code: {r.status_code}, 报错: {r.text}')
389
+ # 返回合并后的结果
390
+ result_data = {
391
+ 'issues': all_issues,
392
+ 'total': len(all_issues),
393
+ 'startAt': 0
394
+ }
395
+
396
+ return ReturnResponse(code=0, msg=f'成功获取 {len(all_issues)} 个唯一 issue(共请求 {page_count} 页)', data=result_data)
350
397
 
351
398
 
352
399
  def get_boards(self) -> ReturnResponse:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytbox
3
- Version: 0.2.6
3
+ Version: 0.2.9
4
4
  Summary: A collection of Python integrations and utilities (Feishu, Dida365, VictoriaMetrics, ...)
5
5
  Author-email: mingming hou <houm01@foxmail.com>
6
6
  License-Expression: MIT
@@ -6,7 +6,7 @@ pytbox/mingdao.py,sha256=afEFJ9NKPdsmAZ4trBEJKl66fMj3Z8TWfaOcomNGhzw,6042
6
6
  pytbox/notion.py,sha256=GRPdZAtyG2I6M6pCFbdrTWDACaPsp1RAXrY_RpWYKus,26572
7
7
  pytbox/onepassword_connect.py,sha256=nD3xTl1ykQ4ct_dCRRF138gXCtk-phPfKYXuOn-P7Z8,3064
8
8
  pytbox/onepassword_sa.py,sha256=08iUcYud3aEHuQcUsem9bWNxdXKgaxFbMy9yvtr-DZQ,6995
9
- pytbox/pyjira.py,sha256=7yWfu_qsQNoEoQWagnriD9QWrMyTC3OCAvGfvjLZP74,22731
9
+ pytbox/pyjira.py,sha256=Str9f7qTeBuy8XtJq2bXBI9ib79aQYCHBlJP1QKtZeo,24605
10
10
  pytbox/vmware.py,sha256=WiH67_3-VCBjXJuh3UueOc31BdZDItiZhkeuPzoRhw4,3975
11
11
  pytbox/alert/alert_handler.py,sha256=WCn4cKahv5G5BFGgmc7dX7BQ38h2kxTgxfRVTwc1O2M,6579
12
12
  pytbox/alert/ping.py,sha256=KEnnXdIRJHvR_rEHPWLBt0wz4cGwmA29Lenlak3Z_1Y,778
@@ -45,7 +45,7 @@ pytbox/cli/formatters/__init__.py,sha256=4o85w4j-A-O1oBLvuE9q8AFiJ2C9rvB3MIKsy5V
45
45
  pytbox/cli/formatters/output.py,sha256=h5WhZlQk1rjmxEj88Jy5ODLcv6L5zfGUhks_3AWIkKU,5455
46
46
  pytbox/common/__init__.py,sha256=3JWfgCQZKZuSH5NCE7OCzKwq82pkyop9l7sH5YSNyfU,122
47
47
  pytbox/database/mongo.py,sha256=AhJ9nCAQHKrrcL-ujeonOwEf3x2QkmT2VhoCdglqJmU,3478
48
- pytbox/database/victoriametrics.py,sha256=vUv2sRTvtdIRiZWrbLmAp6mrey8bJKVZc3bRFBpFnok,15660
48
+ pytbox/database/victoriametrics.py,sha256=iSgYLcINpWK3ry1fWoL32k65j91Ag49kNBWXYxW3NKA,15750
49
49
  pytbox/feishu/client.py,sha256=kwGLseGT_iQUFmSqpuS2_77WmxtHstD64nXvktuQ3B4,5865
50
50
  pytbox/feishu/endpoints.py,sha256=z3nPOZPC2JGDJlO7SusWBpRA33hZZ4Z-GBhI6F8L_u4,40240
51
51
  pytbox/feishu/errors.py,sha256=79qFAHZw7jDj3gnWAjI1-W4tB0q1_aSfdjee4xzXeuI,1179
@@ -56,7 +56,7 @@ pytbox/log/victorialog.py,sha256=gffEiq38adv9sC5oZeMcyKghd3SGfRuqtZOFuqHQF6E,413
56
56
  pytbox/mail/alimail.py,sha256=njKA3PUbIaiKFaxKvUObmklmEEHg2YA-O5rpgsgT5_w,5147
57
57
  pytbox/mail/client.py,sha256=RNgWhhTXFTpD43U4p7hbmnfRdmltuZmbm890gaZTzhI,6278
58
58
  pytbox/mail/mail_detail.py,sha256=6u8DK-7WzYPSuX6TdicSCh2Os_9Ou6Rn9xc6WRvv85M,699
59
- pytbox/network/meraki.py,sha256=054E3C5KzAuXs9aPalvdAOUo6Hc5aOKZSWUaVbPquy4,6112
59
+ pytbox/network/meraki.py,sha256=CgwIXZMjkQXBVpp-kpCUsalXL-dw0YstycByb7Odpv4,19635
60
60
  pytbox/utils/cronjob.py,sha256=b17CY1fmaFTdQojicXAXHliov_JZdmT7cZhtO4v5sCo,3080
61
61
  pytbox/utils/env.py,sha256=gD2-NyL3K3Vg1B1eGeD1hRtlSHPGgF8Oi9mchuQL6_o,646
62
62
  pytbox/utils/load_config.py,sha256=R4pGerBinbewsym41hQ8Z-I5I7gepuEKODjIrli4C08,5043
@@ -65,8 +65,8 @@ pytbox/utils/response.py,sha256=kXjlwt0WVmLRam2eu1shzX2cQ7ux4cCQryaPGYwle5g,1247
65
65
  pytbox/utils/richutils.py,sha256=OT9_q2Q1bthzB0g1GlhZVvM4ZAepJRKL6a_Vsr6vEqo,487
66
66
  pytbox/utils/timeutils.py,sha256=uSKgwt20mVcgIGKLsH2tNum8v3rcpzgmBibPvyPQFgM,20433
67
67
  pytbox/win/ad.py,sha256=-3pWfL3dElz-XoO4j4M9lrgu3KJtlhrS9gCWJBpafAU,1147
68
- pytbox-0.2.6.dist-info/METADATA,sha256=imFjJeLVrjd09kC9dBKWYtVIGXH4hphvPh7mTAQTw9A,6319
69
- pytbox-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
- pytbox-0.2.6.dist-info/entry_points.txt,sha256=YaTOJ2oPjOiv2SZwY0UC-UA9QS2phRH1oMvxGnxO0Js,43
71
- pytbox-0.2.6.dist-info/top_level.txt,sha256=YADgWue-Oe128ptN3J2hS3GB0Ncc5uZaSUM3e1rwswE,7
72
- pytbox-0.2.6.dist-info/RECORD,,
68
+ pytbox-0.2.9.dist-info/METADATA,sha256=-1h0V2CZvPPtUYHMGpQ-vAWlusDx06lJVhHDSxPAGCg,6319
69
+ pytbox-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
+ pytbox-0.2.9.dist-info/entry_points.txt,sha256=YaTOJ2oPjOiv2SZwY0UC-UA9QS2phRH1oMvxGnxO0Js,43
71
+ pytbox-0.2.9.dist-info/top_level.txt,sha256=YADgWue-Oe128ptN3J2hS3GB0Ncc5uZaSUM3e1rwswE,7
72
+ pytbox-0.2.9.dist-info/RECORD,,
File without changes