baiducloud-python-sdk-core 0.0.1__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,762 @@
1
+ # Copyright 2014 Baidu, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
4
+ # except in compliance with the License. You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software distributed under the
9
+ # License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
10
+ # either express or implied. See the License for the specific language governing permissions
11
+ # and limitations under the License.
12
+
13
+ """
14
+ This module provide some tools for bce client.
15
+ """
16
+ # str() generator unicode,bytes() for ASCII
17
+ from __future__ import print_function
18
+ from __future__ import absolute_import
19
+ from builtins import str, bytes
20
+ from future.utils import iteritems, iterkeys, itervalues
21
+ from baiducloud_python_sdk_core import compat
22
+
23
+ import os
24
+ import re
25
+ import datetime
26
+ import hashlib
27
+ import base64
28
+ import string
29
+ import sys
30
+ try:
31
+ from urllib.parse import urlparse
32
+ except ImportError:
33
+ from urlparse import urlparse
34
+ from Crypto.Cipher import AES
35
+ import baiducloud_python_sdk_core
36
+ from baiducloud_python_sdk_core.http import http_headers
37
+ from typing import Any, Dict, List, Union
38
+ import codecs
39
+
40
+ DEFAULT_CNAME_LIKE_LIST = [b".cdn.bcebos.com"]
41
+ DEFAULT_BOS_DOMAIN_SUFFIX = b'bcebos.com'
42
+ HTTP_PROTOCOL_HEAD = b'http'
43
+
44
+ def get_md5_from_fp(fp, offset=0, length=-1, buf_size=8192):
45
+ """
46
+ Get MD5 from file by fp.
47
+
48
+ :type fp: FileIO
49
+ :param fp: None
50
+
51
+ :type offset: long
52
+ :param offset: None
53
+
54
+ :type length: long
55
+ :param length: None
56
+ =======================
57
+ :return:
58
+ **file_size, MD(encode by base64)**
59
+ """
60
+
61
+ origin_offset = fp.tell()
62
+ if offset:
63
+ fp.seek(offset)
64
+ md5 = hashlib.md5()
65
+ while True:
66
+ bytes_to_read = buf_size
67
+ if bytes_to_read > length > 0:
68
+ bytes_to_read = length
69
+ buf = fp.read(bytes_to_read)
70
+ if not buf:
71
+ break
72
+ md5.update(buf)
73
+ if length > 0:
74
+ length -= len(buf)
75
+ if length == 0:
76
+ break
77
+ fp.seek(origin_offset)
78
+ return base64.standard_b64encode(md5.digest())
79
+
80
+
81
+ def get_canonical_time(timestamp=0):
82
+ """
83
+ Get cannonical time.
84
+
85
+ :type timestamp: int
86
+ :param timestamp: None
87
+ =======================
88
+ :return:
89
+ **string of canonical_time**
90
+ """
91
+ if timestamp == 0:
92
+ utctime = datetime.datetime.utcnow()
93
+ else:
94
+ utctime = datetime.datetime.utcfromtimestamp(timestamp)
95
+ return b"%04d-%02d-%02dT%02d:%02d:%02dZ" % (
96
+ utctime.year, utctime.month, utctime.day,
97
+ utctime.hour, utctime.minute, utctime.second)
98
+
99
+
100
+ def is_ip(s):
101
+ """
102
+ Check a string whether is a legal ip address.
103
+
104
+ :type s: string
105
+ :param s: None
106
+ =======================
107
+ :return:
108
+ **Boolean**
109
+ """
110
+ try:
111
+ tmp_list = s.split(b':')
112
+ s = tmp_list[0]
113
+ if s == b'localhost':
114
+ return True
115
+ tmp_list = s.split(b'.')
116
+ if len(tmp_list) != 4:
117
+ return False
118
+ else:
119
+ for i in tmp_list:
120
+ if int(i) < 0 or int(i) > 255:
121
+ return False
122
+ except:
123
+ return False
124
+ return True
125
+
126
+
127
+ def convert_to_standard_string(input_string):
128
+ """
129
+ Encode a string to utf-8.
130
+
131
+ :type input_string: string
132
+ :param input_string: None
133
+ =======================
134
+ :return:
135
+ **string**
136
+ """
137
+ #if isinstance(input_string, str):
138
+ # return input_string.encode(baiducloud_python_sdk_core.DEFAULT_ENCODING)
139
+ #elif isinstance(input_string, bytes):
140
+ # return input_string
141
+ #else:
142
+ # return str(input_string).encode("utf-8")
143
+ return compat.convert_to_bytes(input_string)
144
+
145
+ def convert_header2map(header_list):
146
+ """
147
+ Transfer a header list to dict
148
+
149
+ :type s: list
150
+ :param s: None
151
+ =======================
152
+ :return:
153
+ **dict**
154
+ """
155
+ header_map = {}
156
+ for a, b in header_list:
157
+ if isinstance(a, bytes):
158
+ a = a.strip(b'\"')
159
+ if isinstance(b, bytes):
160
+ b = b.strip(b'\"')
161
+ header_map[a] = b
162
+ return header_map
163
+
164
+
165
+ def safe_get_element(name, container):
166
+ """
167
+ Get element from dict which the lower of key and name are equal.
168
+
169
+ :type name: string
170
+ :param name: None
171
+
172
+ :type container: dict
173
+ :param container: None
174
+ =======================
175
+ :return:
176
+ **Value**
177
+ """
178
+ for k, v in iteritems(container):
179
+ if k.strip().lower() == name.strip().lower():
180
+ return v
181
+ return ""
182
+
183
+
184
+ def check_redirect(res):
185
+ """
186
+ Check whether the response is redirect.
187
+
188
+ :type res: HttpResponse
189
+ :param res: None
190
+
191
+ :return:
192
+ **Boolean**
193
+ """
194
+ is_redirect = False
195
+ try:
196
+ if res.status == 301 or res.status == 302:
197
+ is_redirect = True
198
+ except:
199
+ pass
200
+ return is_redirect
201
+
202
+
203
+ def _get_normalized_char_list():
204
+ """"
205
+ :return:
206
+ **ASCII string**
207
+ """
208
+ ret = ['%%%02X' % i for i in range(256)]
209
+ for ch in string.ascii_letters + string.digits + '.~-_':
210
+ ret[ord(ch)] = ch
211
+ if isinstance(ret[0], str):
212
+ ret = [s.encode("utf-8") for s in ret]
213
+ return ret
214
+ _NORMALIZED_CHAR_LIST = _get_normalized_char_list()
215
+
216
+
217
+ def normalize_string(in_str, encoding_slash=True):
218
+ """
219
+ Encode in_str.
220
+ When encoding_slash is True, don't encode skip_chars, vice versa.
221
+
222
+ :type in_str: string
223
+ :param in_str: None
224
+
225
+ :type encoding_slash: Bool
226
+ :param encoding_slash: None
227
+ ===============================
228
+ :return:
229
+ **ASCII string**
230
+ """
231
+ tmp = []
232
+ for ch in convert_to_standard_string(in_str):
233
+ # on python3, ch is int type
234
+ sep = ''
235
+ index = -1
236
+ if isinstance(ch, int):
237
+ # on py3
238
+ sep = chr(ch).encode("utf-8")
239
+ index = ch
240
+ else:
241
+ sep = ch
242
+ index = ord(ch)
243
+ if sep == b'/' and not encoding_slash:
244
+ tmp.append(b'/')
245
+ else:
246
+ tmp.append(_NORMALIZED_CHAR_LIST[index])
247
+ return (b'').join(tmp)
248
+
249
+
250
+ def append_uri(base_uri, *path_components):
251
+ """
252
+ Append path_components to the end of base_uri in order, and ignore all empty strings and None
253
+
254
+ :param base_uri: None
255
+ :type base_uri: string
256
+
257
+ :param path_components: None
258
+
259
+ :return: the final url
260
+ :rtype: str
261
+ """
262
+ tmp = [base_uri]
263
+ for path in path_components:
264
+ if path:
265
+ tmp.append(normalize_string(path, False))
266
+ if len(tmp) > 1:
267
+ tmp[0] = tmp[0].rstrip(b'/')
268
+ tmp[-1] = tmp[-1].lstrip(b'/')
269
+ for i in range(1, len(tmp) - 1):
270
+ tmp[i] = tmp[i].strip(b'/')
271
+ return (b'/').join(tmp)
272
+
273
+
274
+ def check_bucket_valid(bucket):
275
+ """
276
+ Check bucket name whether is legal.
277
+
278
+ :type bucket: string
279
+ :param bucket: None
280
+ =======================
281
+ :return:
282
+ **Boolean**
283
+ """
284
+ alphabet = "abcdefghijklmnopqrstuvwxyz0123456789-"
285
+ if len(bucket) < 3 or len(bucket) > 63:
286
+ return False
287
+ if bucket[-1] == "-" or bucket[-1] == "_":
288
+ return False
289
+ if not (('a' <= bucket[0] <= 'z') or ('0' <= bucket[0] <= '9')):
290
+ return False
291
+ for i in bucket:
292
+ if not i in alphabet:
293
+ return False
294
+ return True
295
+
296
+
297
+ def guess_content_type_by_file_name(file_name):
298
+ """
299
+ Get file type by filename.
300
+
301
+ :type file_name: string
302
+ :param file_name: None
303
+ =======================
304
+ :return:
305
+ **Type Value**
306
+ """
307
+ mime_map = dict()
308
+ mime_map["js"] = "application/javascript"
309
+ mime_map["xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
310
+ mime_map["xltx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.template"
311
+ mime_map["potx"] = "application/vnd.openxmlformats-officedocument.presentationml.template"
312
+ mime_map["ppsx"] = "application/vnd.openxmlformats-officedocument.presentationml.slideshow"
313
+ mime_map["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
314
+ mime_map["sldx"] = "application/vnd.openxmlformats-officedocument.presentationml.slide"
315
+ mime_map["docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
316
+ mime_map["dotx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.template"
317
+ mime_map["xlam"] = "application/vnd.ms-excel.addin.macroEnabled.12"
318
+ mime_map["xlsb"] = "application/vnd.ms-excel.sheet.binary.macroEnabled.12"
319
+ try:
320
+ file_name = compat.convert_to_string(file_name)
321
+ name = os.path.basename(file_name.lower())
322
+ suffix = name.split('.')[-1]
323
+ if suffix in iterkeys(mime_map):
324
+ mime_type = mime_map[suffix]
325
+ else:
326
+ import mimetypes
327
+
328
+ mimetypes.init()
329
+ mime_type = mimetypes.types_map.get("." + suffix, 'application/octet-stream')
330
+ except:
331
+ mime_type = 'application/octet-stream'
332
+ if not mime_type:
333
+ mime_type = 'application/octet-stream'
334
+
335
+ return compat.convert_to_bytes(mime_type)
336
+
337
+
338
+ _first_cap_regex = re.compile('(.)([A-Z][a-z]+)')
339
+ _number_cap_regex = re.compile('([a-z])([0-9]{2,})')
340
+ _end_cap_regex = re.compile('([a-z0-9])([A-Z])')
341
+
342
+
343
+ def pythonize_name(name):
344
+ """Convert camel case to a "pythonic" name.
345
+ Examples::
346
+ pythonize_name('CamelCase') -> 'camel_case'
347
+ pythonize_name('already_pythonized') -> 'already_pythonized'
348
+ pythonize_name('HTTPRequest') -> 'http_request'
349
+ pythonize_name('HTTPStatus200Ok') -> 'http_status_200_ok'
350
+ pythonize_name('UPPER') -> 'upper'
351
+ pythonize_name('ContentMd5')->'content_md5'
352
+ pythonize_name('') -> ''
353
+ """
354
+ if name == "eTag":
355
+ return "etag"
356
+ s1 = _first_cap_regex.sub(r'\1_\2', name)
357
+ s2 = _number_cap_regex.sub(r'\1_\2', s1)
358
+ return _end_cap_regex.sub(r'\1_\2', s2).lower()
359
+
360
+
361
+ def get_canonical_querystring(params, for_signature):
362
+ """
363
+
364
+ :param params:
365
+ :param for_signature:
366
+ :return:
367
+ """
368
+ if params is None:
369
+ return ''
370
+ result = []
371
+ for k, v in iteritems(params):
372
+ if not for_signature or k.lower != http_headers.AUTHORIZATION.lower():
373
+ if v is None:
374
+ v = ''
375
+ result.append(b'%s=%s' % (normalize_string(k), normalize_string(v)))
376
+ result.sort()
377
+ return (b'&').join(result)
378
+
379
+
380
+ def print_object(obj):
381
+ """
382
+
383
+ :param obj:
384
+ :return:
385
+ """
386
+ tmp = []
387
+ for k, v in iteritems(obj.__dict__):
388
+ if not k.startswith('__') and k != "raw_data":
389
+ if isinstance(v, bytes):
390
+ tmp.append("%s:'%s'" % (k, v))
391
+ # str is unicode
392
+ elif isinstance(v, str):
393
+ tmp.append("%s:u'%s'" % (k, v))
394
+ else:
395
+ tmp.append('%s:%s' % (k, v))
396
+ return '{%s}' % ','.join(tmp)
397
+
398
+ class Expando(object):
399
+ """
400
+ Expandable class
401
+ """
402
+ def __init__(self, attr_dict=None):
403
+ if attr_dict:
404
+ self.__dict__.update(attr_dict)
405
+
406
+ def __getattr__(self, item):
407
+ if item.startswith('__'):
408
+ raise AttributeError
409
+ return None
410
+
411
+ def __repr__(self):
412
+ return print_object(self)
413
+
414
+
415
+ def dict_to_python_object(d):
416
+ """
417
+
418
+ :param d:
419
+ :return:
420
+ """
421
+ attr = {}
422
+ for k, v in iteritems(d):
423
+ if not isinstance(k, compat.string_types):
424
+ k = compat.convert_to_string(k)
425
+ k = pythonize_name(k)
426
+ attr[k] = v
427
+ return Expando(attr)
428
+
429
+
430
+ def required(**types):
431
+ """
432
+ decorator of input param check
433
+ :param types:
434
+ :return:
435
+ """
436
+ def _required(f):
437
+ def _decorated(*args, **kwds):
438
+ for i, v in enumerate(args):
439
+ if f.__code__.co_varnames[i] in types:
440
+ if v is None:
441
+ raise ValueError('arg "%s" should not be None' %
442
+ (f.__code__.co_varnames[i]))
443
+ if not isinstance(v, types[f.__code__.co_varnames[i]]):
444
+ raise TypeError('arg "%s"= %r does not match %s' %
445
+ (f.__code__.co_varnames[i],
446
+ v,
447
+ types[f.__code__.co_varnames[i]]))
448
+ for k, v in iteritems(kwds):
449
+ if k in types:
450
+ if v is None:
451
+ raise ValueError('arg "%s" should not be None' % k)
452
+ if not isinstance(v, types[k]):
453
+ raise TypeError('arg "%s"= %r does not match %s' % (k, v, types[k]))
454
+ return f(*args, **kwds)
455
+ _decorated.__name__ = f.__name__
456
+ return _decorated
457
+ return _required
458
+
459
+
460
+ def parse_host_port(endpoint, default_protocol):
461
+ """
462
+ parse protocol, host, port from endpoint in config
463
+
464
+ :type: string
465
+ :param endpoint: endpoint in config
466
+
467
+ :type: baiducloud_python_sdk_core.protocol.HTTP or baiducloud_python_sdk_core.protocol.HTTPS
468
+ :param default_protocol: if there is no scheme in endpoint,
469
+ we will use this protocol as default
470
+ :return: tuple of protocol, host, port
471
+ """
472
+ # netloc should begin with // according to RFC1808
473
+ if b"//" not in endpoint:
474
+ endpoint = b"//" + endpoint
475
+
476
+ try:
477
+ # scheme in endpoint dominates input default_protocol
478
+ parse_result = urlparse(
479
+ endpoint,
480
+ compat.convert_to_bytes(default_protocol.name))
481
+ except Exception as e:
482
+ raise ValueError('Invalid endpoint:%s, error:%s' % (endpoint,
483
+ compat.convert_to_string(e)))
484
+
485
+ if parse_result.scheme == compat.convert_to_bytes(baiducloud_python_sdk_core.protocol.HTTP.name):
486
+ protocol = baiducloud_python_sdk_core.protocol.HTTP
487
+ port = baiducloud_python_sdk_core.protocol.HTTP.default_port
488
+ elif parse_result.scheme == compat.convert_to_bytes(baiducloud_python_sdk_core.protocol.HTTPS.name):
489
+ protocol = baiducloud_python_sdk_core.protocol.HTTPS
490
+ port = baiducloud_python_sdk_core.protocol.HTTPS.default_port
491
+ else:
492
+ raise ValueError('Unsupported protocol %s' % parse_result.scheme)
493
+ host = parse_result.hostname
494
+ if parse_result.port is not None:
495
+ port = parse_result.port
496
+
497
+ return protocol, host, port
498
+
499
+ """
500
+ def aes128_encrypt_16char_key(adminpass, secretkey):
501
+
502
+ #Python2:encrypt admin password by AES128
503
+
504
+ pad_it = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16)
505
+ key = secretkey[0:16]
506
+ mode = AES.MODE_ECB
507
+ cryptor = AES.new(key, mode, key)
508
+ cipheradminpass = cryptor.encrypt(pad_it(adminpass)).encode('hex')
509
+ return cipheradminpass
510
+ """
511
+
512
+
513
+ def aes128_encrypt_16char_key(adminpass, secretkey):
514
+ """
515
+
516
+ :param adminpass: adminpass
517
+ :param secretkey: secretkey
518
+ :return: cipheradminpass
519
+ """
520
+
521
+ # Python3: encrypt admin password by AES128
522
+
523
+ pad_it = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16)
524
+ key = secretkey[0:16]
525
+ mode = AES.MODE_ECB
526
+ cryptor = AES.new(key, mode)
527
+ pad_admin = pad_it(adminpass)
528
+ byte_pad_admin = pad_admin.encode(encoding='utf-8')
529
+
530
+ cryptoradminpass = cryptor.encrypt(byte_pad_admin)
531
+ #print(cryptoradminpass)
532
+
533
+ #cipheradminpass = cryptor.encrypt(byte_pad_admin).encode('hex')
534
+ byte_cipheradminpass = codecs.encode(cryptoradminpass, 'hex_codec')
535
+ #print(byte_cipheradminpass)
536
+
537
+ cipheradminpass = byte_cipheradminpass.decode(encoding='utf-8')
538
+ #print(cipheradminpass)
539
+
540
+ return cipheradminpass
541
+
542
+ def is_cname_like_host(host):
543
+ """
544
+ :param host: custom domain
545
+ :return: domain end with cdn endpoint or not
546
+ """
547
+ if host is None:
548
+ return False
549
+ for suffix in DEFAULT_CNAME_LIKE_LIST:
550
+ if host.lower().endswith(suffix):
551
+ return True
552
+ return False
553
+
554
+
555
+ def is_custom_host(host, bucket_name):
556
+ """
557
+ custom host : xxx.region.bcebos.com
558
+ : return: custom, domain or not
559
+ """
560
+ if host is None or bucket_name is None:
561
+ return False
562
+
563
+ host_split = host.split(b'.')
564
+ # split http head
565
+ return host.lower().startswith(compat.convert_to_bytes(bucket_name.lower())) \
566
+ and len(host_split) == 4 and is_bos_suffixed_host(host)
567
+
568
+
569
+ def is_bos_suffixed_host(host):
570
+ """
571
+ :param host: bos endpoint
572
+ :return: bos endpoint or not
573
+ """
574
+ if host is None:
575
+ return False
576
+ if host.endswith(b'/'):
577
+ check_host = host[:-1]
578
+ else:
579
+ check_host = host
580
+
581
+ return check_host.lower().endswith(DEFAULT_BOS_DOMAIN_SUFFIX)
582
+
583
+
584
+ def check_ipv4(ipAddr):
585
+ """
586
+ :param ipAddr: ip address
587
+ :return: true or false
588
+ """
589
+ compile_ip=re.compile(b'((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}')
590
+ return compile_ip.match(ipAddr)
591
+
592
+ def _get_data_size(data):
593
+ if hasattr(data, '__len__'):
594
+ return len(data)
595
+
596
+ if hasattr(data, 'len'):
597
+ return data.len
598
+
599
+ if hasattr(data, 'seek') and hasattr(data, 'tell'):
600
+ return file_object_remaining_bytes(data)
601
+
602
+ return None
603
+
604
+ def file_object_remaining_bytes(fileobj):
605
+ current = fileobj.tell()
606
+
607
+ fileobj.seek(0, os.SEEK_END)
608
+ end = fileobj.tell()
609
+ fileobj.seek(current, os.SEEK_SET)
610
+
611
+ return end - current
612
+
613
+ def _invoke_progress_callback(progress_callback, consumed_bytes, total_bytes):
614
+ if progress_callback:
615
+ progress_callback(consumed_bytes, total_bytes)
616
+
617
+ def make_progress_adapter(data, progress_callback, size=None):
618
+ """return a adapter,when reading 'data', that is, calling read or iterating
619
+ over it Call the progress callback function
620
+
621
+ :param data: bytes,file object or iterable
622
+ :param progress_callback: callback function, ref:`_default_progress_callback`
623
+ :param size: size of `data`
624
+
625
+ :return: callback function adapter
626
+ """
627
+
628
+ if size is None:
629
+ size = _get_data_size(data)
630
+
631
+ if size is None:
632
+ raise ValueError('{0} is not a file object'.format(data.__class__.__name__))
633
+
634
+ return _BytesAndFileAdapter(data, progress_callback, size)
635
+
636
+ _CHUNK_SIZE = 8 * 1024
637
+
638
+ class _BytesAndFileAdapter(object):
639
+ """With this adapter, you can add progress monitoring to 'data'.
640
+
641
+ :param data: bytes or file object
642
+ :param progress_callback: user-provided callback function. like callback(bytes_read, total_bytes)
643
+ bytes_read is readed bytes;total_bytes is total bytes
644
+ :param int size : data size
645
+ """
646
+ def __init__(self, data, progress_callback=None, size=None):
647
+ self.data = data
648
+ self.progress_callback = progress_callback
649
+ self.size = size
650
+ self.offset = 0
651
+
652
+ @property
653
+ def len(self):
654
+ return self.size
655
+
656
+ # for python 2.x
657
+ def __bool__(self):
658
+ return True
659
+ # for python 3.x
660
+ __nonzero__=__bool__
661
+
662
+ # support iterable type
663
+ # def __iter__(self):
664
+ # return self
665
+
666
+ # def __next__(self):
667
+ # return self.next()
668
+
669
+ # def next(self):
670
+ # content = self.read(_CHUNK_SIZE)
671
+
672
+ # if content:
673
+ # return content
674
+ # else:
675
+ # raise StopIteration
676
+
677
+ def read(self, amt=None):
678
+ if self.offset >= self.size:
679
+ return compat.convert_to_bytes('')
680
+
681
+ if amt is None or amt < 0:
682
+ bytes_to_read = self.size - self.offset
683
+ else:
684
+ bytes_to_read = min(amt, self.size - self.offset)
685
+
686
+ if isinstance(self.data, bytes):
687
+ content = self.data[self.offset:self.offset+bytes_to_read]
688
+ else:
689
+ content = self.data.read(bytes_to_read)
690
+
691
+ self.offset += bytes_to_read
692
+
693
+ _invoke_progress_callback(self.progress_callback, min(self.offset, self.size), self.size)
694
+
695
+ return content
696
+
697
+ def default_progress_callback(consumed_bytes, total_bytes):
698
+ """Progress bar callback function that calculates the percentage of current completion
699
+
700
+ :param consumed_bytes: Amount of data that has been uploaded/downloaded
701
+ :param total_bytes: According to the total amount
702
+ """
703
+ if total_bytes:
704
+ rate = int(100 * (float(consumed_bytes) / float(total_bytes)))
705
+ start_progress = '*' * rate
706
+ end_progress = '.' * (100 - rate)
707
+ if rate == 100:
708
+ print("\r{}%[{}->{}]\n".format(rate, start_progress, end_progress), end="")
709
+ else:
710
+ print("\r{}%[{}->{}]".format(rate, start_progress, end_progress), end="")
711
+
712
+ sys.stdout.flush()
713
+
714
+ def to_dict(obj, visited=None):
715
+ if obj is None:
716
+ return None
717
+ if visited is None:
718
+ visited = set()
719
+ # 避免循环引用
720
+ if id(obj) in visited:
721
+ return str(obj) # 或者 return None / "CyclicRef"
722
+ visited.add(id(obj))
723
+ if isinstance(obj, (str, int, float, bool)):
724
+ return obj
725
+ elif isinstance(obj, list):
726
+ return [to_dict(v, visited) for v in obj]
727
+ elif isinstance(obj, dict):
728
+ return {k: to_dict(v, visited) for k, v in obj.items()}
729
+ elif hasattr(obj, "__dict__"): # 任意 class 实例
730
+ result = {}
731
+ for key, value in obj.__dict__.items():
732
+ result[key] = to_dict(value, visited)
733
+ return result
734
+ else:
735
+ return obj
736
+
737
+ def to_api_dict(d: Dict[str, Any]) -> Dict[str, Any]:
738
+ """
739
+ 清理字典:
740
+ - 移除 value 为 None 的键值对
741
+ - 支持递归清理 dict 和 list
742
+ - list 内部的 None 不移除,但会递归处理其中的 dict 或对象
743
+ - 普通类对象会转为 __dict__
744
+ """
745
+ if not isinstance(d, dict):
746
+ raise TypeError("Input must be a dict")
747
+ def _to_dict(obj: Any) -> Any:
748
+ if hasattr(obj, "__dict__"):
749
+ return obj.__dict__
750
+ return obj
751
+ def _clean(obj: Union[Dict[str, Any], List[Any], Any]) -> Any:
752
+ if isinstance(obj, dict):
753
+ return {k: _clean(v) for k, v in obj.items() if v is not None}
754
+ elif isinstance(obj, list):
755
+ return [
756
+ _clean(item) if isinstance(item, (dict, list))
757
+ else _clean(_to_dict(item))
758
+ for item in obj
759
+ ]
760
+ else:
761
+ return _to_dict(obj)
762
+ return _clean(d)