appier 1.34.7__py2.py3-none-any.whl → 1.34.8__py2.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.
appier/http.py CHANGED
@@ -1,1283 +1,1292 @@
1
- #!/usr/bin/python
2
- # -*- coding: utf-8 -*-
3
-
4
- # Hive Appier Framework
5
- # Copyright (c) 2008-2024 Hive Solutions Lda.
6
- #
7
- # This file is part of Hive Appier Framework.
8
- #
9
- # Hive Appier Framework is free software: you can redistribute it and/or modify
10
- # it under the terms of the Apache License as published by the Apache
11
- # Foundation, either version 2.0 of the License, or (at your option) any
12
- # later version.
13
- #
14
- # Hive Appier Framework is distributed in the hope that it will be useful,
15
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
- # Apache License for more details.
18
- #
19
- # You should have received a copy of the Apache License along with
20
- # Hive Appier Framework. If not, see <http://www.apache.org/licenses/>.
21
-
22
- __author__ = "João Magalhães <joamag@hive.pt>"
23
- """ The author(s) of the module """
24
-
25
- __copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
26
- """ The copyright for the module """
27
-
28
- __license__ = "Apache License, Version 2.0"
29
- """ The license for the module """
30
-
31
- import os
32
- import json
33
- import base64
34
- import string
35
- import random
36
- import logging
37
- import threading
38
-
39
- from . import util
40
- from . import common
41
- from . import legacy
42
- from . import typesf
43
- from . import config
44
- from . import exceptions
45
- from . import structures
46
-
47
- TIMEOUT = 60
48
- """ The timeout in seconds to be used for the blocking
49
- operations in the HTTP connection, this value avoid unwanted
50
- blocking operations to remain open for an infinite time """
51
-
52
- RANGE = string.ascii_letters + string.digits
53
- """ The range of characters that are going to be used in
54
- the generation of the boundary value for the mime """
55
-
56
- SEQUENCE_TYPES = (list, tuple)
57
- """ The sequence defining the various types that are
58
- considered to be sequence based for python """
59
-
60
- AUTH_ERRORS = (401, 403, 440, 499)
61
- """ The sequence that defines the various HTTP errors
62
- considered to be authentication related and for which a
63
- new authentication try will be performed """
64
-
65
- ACCESS_LOCK = threading.RLock()
66
- """ Global access lock used for locking global operations
67
- that require thread safety under the HTTP infra-structure """
68
-
69
-
70
- def file_g(path, chunk=40960):
71
- yield os.path.getsize(path)
72
- file = open(path, "rb")
73
- try:
74
- while True:
75
- data = file.read(chunk)
76
- if not data:
77
- break
78
- yield data
79
- finally:
80
- file.close()
81
-
82
-
83
- def get_f(*args, **kwargs):
84
- name = kwargs.pop("name", "default")
85
- kwargs["handle"] = kwargs.get("handle", True)
86
- kwargs["redirect"] = kwargs.get("redirect", True)
87
- data, response = get(*args, **kwargs)
88
- info = response.info()
89
- mime = info.get("Content-Type", None)
90
- file_tuple = util.FileTuple((name, mime, data))
91
- return typesf.File(file_tuple)
92
-
93
-
94
- def get(
95
- url,
96
- params=None,
97
- headers=None,
98
- handle=None,
99
- silent=None,
100
- redirect=None,
101
- timeout=None,
102
- auth_callback=None,
103
- **kwargs
104
- ):
105
- return _method(
106
- _get,
107
- url,
108
- params=params,
109
- headers=headers,
110
- handle=handle,
111
- silent=silent,
112
- redirect=redirect,
113
- timeout=timeout,
114
- auth_callback=auth_callback,
115
- **kwargs
116
- )
117
-
118
-
119
- def post(
120
- url,
121
- params=None,
122
- data=None,
123
- data_j=None,
124
- data_m=None,
125
- headers=None,
126
- mime=None,
127
- handle=None,
128
- silent=None,
129
- redirect=None,
130
- timeout=None,
131
- auth_callback=None,
132
- **kwargs
133
- ):
134
- return _method(
135
- _post,
136
- url,
137
- params=params,
138
- data=data,
139
- data_j=data_j,
140
- data_m=data_m,
141
- headers=headers,
142
- mime=mime,
143
- handle=handle,
144
- silent=silent,
145
- redirect=redirect,
146
- timeout=timeout,
147
- auth_callback=auth_callback,
148
- **kwargs
149
- )
150
-
151
-
152
- def put(
153
- url,
154
- params=None,
155
- data=None,
156
- data_j=None,
157
- data_m=None,
158
- headers=None,
159
- mime=None,
160
- handle=None,
161
- silent=None,
162
- redirect=None,
163
- timeout=None,
164
- auth_callback=None,
165
- **kwargs
166
- ):
167
- return _method(
168
- _put,
169
- url,
170
- params=params,
171
- data=data,
172
- data_j=data_j,
173
- data_m=data_m,
174
- headers=headers,
175
- mime=mime,
176
- handle=handle,
177
- silent=silent,
178
- redirect=redirect,
179
- timeout=timeout,
180
- auth_callback=auth_callback,
181
- **kwargs
182
- )
183
-
184
-
185
- def delete(
186
- url,
187
- params=None,
188
- headers=None,
189
- handle=None,
190
- silent=None,
191
- redirect=None,
192
- timeout=None,
193
- auth_callback=None,
194
- **kwargs
195
- ):
196
- return _method(
197
- _delete,
198
- url,
199
- params=params,
200
- headers=headers,
201
- handle=handle,
202
- silent=silent,
203
- redirect=redirect,
204
- timeout=timeout,
205
- auth_callback=auth_callback,
206
- **kwargs
207
- )
208
-
209
-
210
- def patch(
211
- url,
212
- params=None,
213
- data=None,
214
- data_j=None,
215
- data_m=None,
216
- headers=None,
217
- mime=None,
218
- handle=None,
219
- silent=None,
220
- redirect=None,
221
- timeout=None,
222
- auth_callback=None,
223
- **kwargs
224
- ):
225
- return _method(
226
- _patch,
227
- url,
228
- params=params,
229
- data=data,
230
- data_j=data_j,
231
- data_m=data_m,
232
- headers=headers,
233
- mime=mime,
234
- handle=handle,
235
- silent=silent,
236
- redirect=redirect,
237
- timeout=timeout,
238
- auth_callback=auth_callback,
239
- **kwargs
240
- )
241
-
242
-
243
- def basic_auth(username, password=None):
244
- if not password:
245
- password = username
246
- authorization = _authorization(username, password)
247
- return "Basic %s" % authorization
248
-
249
-
250
- def _try_auth(auth_callback, params, headers=None):
251
- if not auth_callback:
252
- raise
253
- if headers == None:
254
- headers = dict()
255
- auth_callback(params, headers)
256
-
257
-
258
- def _method(method, *args, **kwargs):
259
- try:
260
- auth_callback = kwargs.pop("auth_callback", None)
261
- result = method(*args, **kwargs)
262
- except legacy.HTTPError as error:
263
- try:
264
- params = kwargs.get("params", None)
265
- headers = kwargs.get("headers", None)
266
- if not error.code in AUTH_ERRORS:
267
- raise
268
- _try_auth(auth_callback, params, headers)
269
- result = method(*args, **kwargs)
270
- except legacy.HTTPError as error:
271
- code = error.getcode()
272
- raise exceptions.HTTPError(error, code)
273
-
274
- return result
275
-
276
-
277
- def _get(
278
- url,
279
- params=None,
280
- headers=None,
281
- handle=None,
282
- silent=None,
283
- redirect=None,
284
- timeout=None,
285
- **kwargs
286
- ):
287
- return _method_empty(
288
- "GET",
289
- url,
290
- params=params,
291
- headers=headers,
292
- handle=handle,
293
- silent=silent,
294
- redirect=redirect,
295
- timeout=timeout,
296
- **kwargs
297
- )
298
-
299
-
300
- def _post(
301
- url,
302
- params=None,
303
- data=None,
304
- data_j=None,
305
- data_m=None,
306
- headers=None,
307
- mime=None,
308
- handle=None,
309
- silent=None,
310
- redirect=None,
311
- timeout=None,
312
- **kwargs
313
- ):
314
- return _method_payload(
315
- "POST",
316
- url,
317
- params=params,
318
- data=data,
319
- data_j=data_j,
320
- data_m=data_m,
321
- headers=headers,
322
- mime=mime,
323
- handle=handle,
324
- silent=silent,
325
- redirect=redirect,
326
- timeout=timeout,
327
- **kwargs
328
- )
329
-
330
-
331
- def _put(
332
- url,
333
- params=None,
334
- data=None,
335
- data_j=None,
336
- data_m=None,
337
- headers=None,
338
- mime=None,
339
- handle=None,
340
- silent=None,
341
- redirect=None,
342
- timeout=None,
343
- **kwargs
344
- ):
345
- return _method_payload(
346
- "PUT",
347
- url,
348
- params=params,
349
- data=data,
350
- data_j=data_j,
351
- data_m=data_m,
352
- headers=headers,
353
- mime=mime,
354
- handle=handle,
355
- silent=silent,
356
- redirect=redirect,
357
- timeout=timeout,
358
- **kwargs
359
- )
360
-
361
-
362
- def _delete(
363
- url,
364
- params=None,
365
- headers=None,
366
- handle=None,
367
- silent=None,
368
- redirect=None,
369
- timeout=None,
370
- **kwargs
371
- ):
372
- return _method_empty(
373
- "DELETE",
374
- url,
375
- params=params,
376
- headers=headers,
377
- handle=handle,
378
- silent=silent,
379
- redirect=redirect,
380
- timeout=timeout,
381
- **kwargs
382
- )
383
-
384
-
385
- def _patch(
386
- url,
387
- params=None,
388
- data=None,
389
- data_j=None,
390
- data_m=None,
391
- headers=None,
392
- mime=None,
393
- handle=None,
394
- silent=None,
395
- redirect=None,
396
- timeout=None,
397
- **kwargs
398
- ):
399
- return _method_payload(
400
- "PATCH",
401
- url,
402
- params=params,
403
- data=data,
404
- data_j=data_j,
405
- data_m=data_m,
406
- headers=headers,
407
- mime=mime,
408
- handle=handle,
409
- silent=silent,
410
- redirect=redirect,
411
- timeout=timeout,
412
- **kwargs
413
- )
414
-
415
-
416
- def _method_empty(
417
- name,
418
- url,
419
- params=None,
420
- headers=None,
421
- handle=None,
422
- silent=None,
423
- redirect=None,
424
- timeout=None,
425
- **kwargs
426
- ):
427
- if handle == None:
428
- handle = False
429
- if silent == None:
430
- silent = config.conf("HTTP_SILENT", False, cast=bool)
431
- if redirect == None:
432
- redirect = config.conf("HTTP_REDIRECT", False, cast=bool)
433
- if timeout == None:
434
- timeout = config.conf("HTTP_TIMEOUT", TIMEOUT, cast=int)
435
- values = params or dict()
436
-
437
- values_s = " with '%s'" % str(values) if values else ""
438
- if not silent:
439
- logging.debug("%s %s%s" % (name, url, values_s))
440
-
441
- url, scheme, host, authorization, extra = _parse_url(url)
442
- if extra:
443
- values.update(extra)
444
- data = _urlencode(values)
445
-
446
- headers = dict(headers) if headers else dict()
447
- if host:
448
- headers["Host"] = host
449
- if authorization:
450
- headers["Authorization"] = "Basic %s" % authorization
451
- url = url + "?" + data if data else url
452
- url = str(url)
453
-
454
- _method_callback(handle, kwargs)
455
-
456
- # runs the concrete resolution method (taking into account the adapter)
457
- # providing it with the required parameters for request execution
458
- file = _resolve(url, name, headers, None, silent, timeout, **kwargs)
459
-
460
- # verifies if the resulting "file" from the resolution process is either
461
- # an invalid or tuple value and if that's the case as the request has
462
- # probably been deferred for asynchronous execution
463
- if file == None:
464
- return file
465
- if isinstance(file, tuple):
466
- return file
467
-
468
- try:
469
- result = file.read()
470
- finally:
471
- file.close()
472
-
473
- code = file.getcode()
474
- info = file.info()
475
-
476
- location = info.get("Location", None) if redirect else None
477
- if location:
478
- return _redirect(
479
- location,
480
- scheme,
481
- host,
482
- handle=handle,
483
- silent=silent,
484
- redirect=redirect,
485
- timeout=timeout,
486
- **kwargs
487
- )
488
-
489
- if not silent:
490
- logging.debug("%s %s returned '%d'" % (name, url, code))
491
-
492
- result = _result(result, info)
493
- return (result, file) if handle else result
494
-
495
-
496
- def _method_payload(
497
- name,
498
- url,
499
- params=None,
500
- data=None,
501
- data_j=None,
502
- data_m=None,
503
- headers=None,
504
- mime=None,
505
- handle=None,
506
- silent=None,
507
- redirect=None,
508
- timeout=None,
509
- **kwargs
510
- ):
511
- if handle == None:
512
- handle = False
513
- if silent == None:
514
- silent = config.conf("HTTP_SILENT", False, cast=bool)
515
- if redirect == None:
516
- redirect = config.conf("HTTP_REDIRECT", False, cast=bool)
517
- if timeout == None:
518
- timeout = config.conf("HTTP_TIMEOUT", TIMEOUT, cast=int)
519
- values = params or dict()
520
-
521
- values_s = " with '%s'" % str(values) if values else ""
522
- if not silent:
523
- logging.debug("%s %s%s" % (name, url, values_s))
524
-
525
- url, scheme, host, authorization, extra = _parse_url(url)
526
- if extra:
527
- values.update(extra)
528
- data_e = _urlencode(values)
529
-
530
- if not data == None:
531
- url = url + "?" + data_e if data_e else url
532
- elif not data_j == None:
533
- data = json.dumps(data_j)
534
- url = url + "?" + data_e if data_e else url
535
- mime = mime or "application/json"
536
- elif not data_m == None:
537
- url = url + "?" + data_e if data_e else url
538
- content_type, data = _encode_multipart(data_m, mime=mime, doseq=True)
539
- mime = content_type
540
- elif data_e:
541
- data = data_e
542
- mime = mime or "application/x-www-form-urlencoded"
543
-
544
- if legacy.is_unicode(data):
545
- data = legacy.bytes(data, force=True)
546
-
547
- if not data:
548
- length = 0
549
- elif legacy.is_bytes(data):
550
- length = len(data)
551
- else:
552
- length = -1
553
-
554
- headers = dict(headers) if headers else dict()
555
- if not length == -1:
556
- headers["Content-Length"] = str(length)
557
- if mime:
558
- headers["Content-Type"] = mime
559
- if host:
560
- headers["Host"] = host
561
- if authorization:
562
- headers["Authorization"] = "Basic %s" % authorization
563
- url = str(url)
564
-
565
- _method_callback(handle, kwargs)
566
- file = _resolve(url, name, headers, data, silent, timeout, **kwargs)
567
- if file == None:
568
- return file
569
-
570
- try:
571
- result = file.read()
572
- finally:
573
- file.close()
574
-
575
- code = file.getcode()
576
- info = file.info()
577
-
578
- location = info.get("Location", None) if redirect else None
579
- if location:
580
- return _redirect(
581
- location,
582
- scheme,
583
- host,
584
- handle=handle,
585
- silent=silent,
586
- redirect=redirect,
587
- timeout=timeout,
588
- )
589
-
590
- if not silent:
591
- logging.debug("%s %s returned '%d'" % (name, url, code))
592
-
593
- result = _result(result, info)
594
- return (result, file) if handle else result
595
-
596
-
597
- def _method_callback(handle, kwargs):
598
- # tries to determine if a callback value has been registered
599
- # in the set of keyword argument and if that's not the case
600
- # returns immediately (nothing to be done)
601
- callback = kwargs.get("callback", None)
602
- if not callback:
603
- return
604
-
605
- def callback_wrap(file):
606
- # determines if the received file is valid (no error)
607
- # or if instead there's an error with the connection
608
- # and an invalid/unset value has been provided
609
- if file:
610
- info = file.info()
611
- try:
612
- result = file.read()
613
- finally:
614
- file.close()
615
- result = _result(result, info)
616
- else:
617
- result = None
618
-
619
- # taking into account the handle flag determines the
620
- # kind of result structure that should be "returned"
621
- result = (result, file) if handle else (result,)
622
- callback(*result)
623
-
624
- # sets the "new" callback clojure in the set of keyword
625
- # based arguments for the calling of the HTTP handler method
626
- # so that this new callback is called instead of the original
627
- kwargs["callback"] = callback_wrap
628
-
629
-
630
- def _redirect(
631
- location,
632
- scheme,
633
- host,
634
- handle=None,
635
- silent=None,
636
- redirect=None,
637
- timeout=None,
638
- **kwargs
639
- ):
640
- is_relative = location.startswith("/")
641
- if is_relative:
642
- location = scheme + "://" + host + location
643
- logging.debug("Redirecting to %s" % location)
644
- return get(
645
- location,
646
- handle=handle,
647
- silent=silent,
648
- redirect=redirect,
649
- timeout=timeout,
650
- **kwargs
651
- )
652
-
653
-
654
- def _resolve(*args, **kwargs):
655
- # obtains the reference to the global set of variables, so
656
- # that it's possible to obtain the proper resolver method
657
- # according to the requested client
658
- _global = globals()
659
-
660
- # tries to retrieve the global configuration values that
661
- # will condition the way the request is going to be performed
662
- client = config.conf("HTTP_CLIENT", "netius")
663
- reuse = config.conf("HTTP_REUSE", True, cast=bool)
664
-
665
- # tries to determine the set of configurations requested on
666
- # a request basis (not global) these have priority when
667
- # compared with the global configuration ones
668
- client = kwargs.pop("client", client)
669
- reuse = kwargs.pop("reuse", reuse)
670
-
671
- # sets the value for connection re-usage so that a connection
672
- # pool is used if request, otherwise one connection per request
673
- # is going to be used (at the expense of resources)
674
- kwargs["reuse"] = reuse
675
-
676
- # tries to retrieve the reference to the resolve method for the
677
- # current client and then runs it, retrieve then the final result,
678
- # note that the result structure may be engine dependent
679
- resolver = _global.get("_resolve_" + client, _resolve_legacy)
680
- try:
681
- result = resolver(*args, **kwargs)
682
- except ImportError:
683
- result = _resolve_legacy(*args, **kwargs)
684
- return result
685
-
686
-
687
- def _resolve_legacy(url, method, headers, data, silent, timeout, **kwargs):
688
- is_generator = not data == None and legacy.is_generator(data)
689
- if is_generator:
690
- next(data)
691
- data = b"".join(data)
692
- is_file = hasattr(data, "tell")
693
- if is_file:
694
- data = data.read()
695
- opener = legacy.build_opener(legacy.HTTPHandler)
696
- request = legacy.Request(url, data=data, headers=headers)
697
- request.get_method = lambda: method
698
- return opener.open(request, timeout=timeout)
699
-
700
-
701
- def _resolve_requests(url, method, headers, data, silent, timeout, **kwargs):
702
- util.ensure_pip("requests")
703
- import requests
704
-
705
- global _requests_session
706
-
707
- # retrieves the various dynamic parameters for the HTTP client
708
- # usage under the requests infra-structure
709
- reuse = kwargs.get("reuse", True)
710
- connections = kwargs.get("connections", 256)
711
-
712
- # verifies if the provided data is a generator, assumes that if the
713
- # data is not invalid and is of type generator then it's a generator
714
- # and then if that's the case encapsulates this size based generator
715
- # into a generator based file-like object so that it can be used inside
716
- # the request infra-structure (as it accepts only file objects)
717
- is_generator = not data == None and legacy.is_generator(data)
718
- if is_generator:
719
- data = structures.GeneratorFile(data)
720
-
721
- # verifies if the session for the requests infra-structure is
722
- # already created and if that's not the case and the re-use
723
- # flag is sets creates a new session for the requested settings
724
- registered = "_requests_session" in globals()
725
- if not registered and reuse:
726
- _requests_session = requests.Session()
727
- adapter = requests.adapters.HTTPAdapter(
728
- pool_connections=connections, pool_maxsize=connections
729
- )
730
- _requests_session.mount("", adapter)
731
-
732
- # determines the based object from which the concrete methods
733
- # are going to be loaded by inspecting the re-use flag
734
- if reuse:
735
- base = _requests_session
736
- else:
737
- base = requests
738
-
739
- # converts the string based method value into a lower cased value
740
- # and then uses it to retrieve the method object method (callable)
741
- # that is going to be called to perform the request
742
- method = method.lower()
743
- caller = getattr(base, method)
744
-
745
- # runs the caller method (according to selected method) and waits for
746
- # the result object converting it then to the target response object
747
- result = caller(url, headers=headers, data=data, timeout=timeout)
748
- response = HTTPResponse(
749
- data=result.content, code=result.status_code, headers=result.headers
750
- )
751
-
752
- # retrieves the response code of the created response and verifies if
753
- # it represent an error, if that's the case raised an error exception
754
- # to the upper layers to break the current execution logic properly
755
- code = response.getcode()
756
- is_error = _is_error(code)
757
- if is_error:
758
- raise legacy.HTTPError(url, code, "HTTP retrieval problem", None, response)
759
-
760
- # returns the final response object to the caller method, this object
761
- # should comply with the proper upper layers structure
762
- return response
763
-
764
-
765
- def _resolve_netius(url, method, headers, data, silent, timeout, **kwargs):
766
- util.ensure_pip("netius")
767
- import netius.clients
768
-
769
- # determines the final value of the silent flag taking into
770
- # account if the current infra-structure is not running under
771
- # a development environment
772
- silent = silent or False
773
- silent |= not common.is_devel()
774
-
775
- # converts the provided dictionary of headers into a new map to
776
- # allow any re-writing of values, valuable for a re-connect
777
- headers = dict(headers)
778
-
779
- # tries to determine the proper level of verbosity to be used by
780
- # the client, for that the system tries to determine if the current
781
- # execution environment is a development one (verbose)
782
- level = logging.CRITICAL if silent else logging.DEBUG
783
-
784
- # retrieves the various dynamic parameters for the HTTP client
785
- # usage under the netius infra-structure
786
- retry = kwargs.get("retry", 1)
787
- reuse = kwargs.get("reuse", True)
788
- level = kwargs.get("level", level)
789
- asynchronous = kwargs.get("async", False)
790
- asynchronous = kwargs.get("asynchronous", asynchronous)
791
- use_file = kwargs.get("use_file", False)
792
- callback = kwargs.get("callback", None)
793
- callback_init = kwargs.get("callback_init", None)
794
- callback_open = kwargs.get("callback_open", None)
795
- callback_headers = kwargs.get("callback_headers", None)
796
- callback_data = kwargs.get("callback_data", None)
797
- callback_result = kwargs.get("callback_result", None)
798
-
799
- # re-calculates the retry and re-use flags taking into account
800
- # the async flag, if the execution mode is async we don't want
801
- # to re-use the HTTP client as it would create issues
802
- retry, reuse = (0, False) if asynchronous else (retry, reuse)
803
-
804
- # creates the proper set of extra parameters to be sent to the
805
- # HTTP client taking into account a possible async method request
806
- extra = (
807
- _async_netius(
808
- callback=callback,
809
- callback_init=callback_init,
810
- callback_open=callback_open,
811
- callback_headers=callback_headers,
812
- callback_data=callback_data,
813
- callback_result=callback_result,
814
- )
815
- if asynchronous
816
- else dict(
817
- on_init=lambda c: callback_init and callback_init(c),
818
- on_open=lambda c: callback_open and callback_open(c),
819
- on_headers=lambda c, p: callback_headers and callback_headers(p.headers),
820
- on_data=lambda c, p, d: callback_data and callback_data(d),
821
- on_result=lambda c, p, r: callback_result and callback_result(r),
822
- )
823
- )
824
-
825
- # verifies if client re-usage must be enforced and if that's the
826
- # case the global client object is requested (singleton) otherwise
827
- # the client should be created inside the HTTP client static method
828
- http_client = _client_netius(level=level) if reuse else None
829
- result = netius.clients.HTTPClient.method_s(
830
- method,
831
- url,
832
- headers=headers,
833
- data=data,
834
- asynchronous=asynchronous,
835
- timeout=timeout,
836
- use_file=use_file,
837
- http_client=http_client,
838
- level=level,
839
- **extra
840
- )
841
-
842
- # if the async mode is defined the result (tuple) is returned immediately
843
- # as the processing will be taking place latter (on callback)
844
- if asynchronous:
845
- return result
846
-
847
- # tries to retrieve any possible error coming from the result object
848
- # if this happens it means an exception has been raised internally and
849
- # the error should be handled in a proper manner, if the error is related
850
- # to a closed connection a retry may be performed to try to re-establish
851
- # the connection (allows for reconnection in connection pool)
852
- error = result.get("error", None)
853
- if error == "closed" and retry > 0:
854
- kwargs["retry"] = retry - 1
855
- return _resolve_netius(url, method, headers, data, silent, timeout, **kwargs)
856
-
857
- # converts the netius specific result map into a response compatible
858
- # object (equivalent to the urllib one) to be used by the upper layers
859
- # under an equivalent and compatible approach note that this conversion
860
- # may raise an exception in case the result represent an error
861
- response = netius.clients.HTTPClient.to_response(result)
862
-
863
- # retrieves the response code of the created response and verifies if
864
- # it represent an error, if that's the case raised an error exception
865
- # to the upper layers to break the current execution logic properly
866
- code = response.getcode()
867
- is_error = _is_error(code)
868
- if is_error:
869
- raise legacy.HTTPError(url, code, "HTTP retrieval problem", None, response)
870
-
871
- # returns the final response object to the upper layers, this object
872
- # may be used freely under the compatibility interface it provides
873
- return response
874
-
875
-
876
- def _client_netius(level=logging.CRITICAL):
877
- import netius.clients
878
-
879
- global _netius_clients
880
-
881
- # retrieves the reference to the current thread and uses the value
882
- # to retrieve the thread identifier (TID) for it, to be used in the
883
- # identification of the client resource associated with it
884
- tid = threading.current_thread().ident
885
-
886
- # acquires the global HTTP lock and executes a series of validation
887
- # and initialization of the netius client infra-structure, this
888
- # operations required thread safety
889
- ACCESS_LOCK.acquire()
890
- try:
891
- registered = "_netius_clients" in globals()
892
- _netius_clients = _netius_clients if registered else dict()
893
- netius_client = _netius_clients.get(tid, None)
894
- finally:
895
- ACCESS_LOCK.release()
896
-
897
- # in case a previously created netius client has been retrieved
898
- # returns it to the caller method for proper re-usage
899
- if netius_client:
900
- return netius_client
901
-
902
- # creates the "new" HTTP client for the current thread and registers
903
- # it under the netius client structure so that it may be re-used
904
- netius_client = netius.clients.HTTPClient(auto_release=False)
905
- _netius_clients[tid] = netius_client
906
-
907
- # in case this is the first registration of the dictionary a new on
908
- # exit callback is registered to cleanup the netius infra-structure
909
- # then the final client is returned to the caller of the method
910
- if not registered:
911
- common.base().on_exit(_cleanup_netius)
912
- return netius_client
913
-
914
-
915
- def _async_netius(
916
- callback=None,
917
- callback_init=None,
918
- callback_open=None,
919
- callback_headers=None,
920
- callback_data=None,
921
- callback_result=None,
922
- ):
923
- import netius.clients
924
-
925
- buffer = []
926
- extra = dict()
927
-
928
- def _on_init(protocol):
929
- callback_init and callback_init(protocol)
930
-
931
- def _on_open(protocol):
932
- callback_open and callback_open(protocol)
933
-
934
- def _on_close(protocol):
935
- callback and callback(None)
936
-
937
- def _on_headers(protocol, parser):
938
- callback_headers and callback_headers(parser.headers)
939
-
940
- def _on_data(protocol, parser, data):
941
- data = data
942
- data and buffer.append(data)
943
- callback_data and callback_data(data)
944
-
945
- def _on_result(protocol, parser, result):
946
- callback_result and callback_result(result)
947
-
948
- def _callback(protocol, parser, message):
949
- result = netius.clients.HTTPProtocol.set_request(parser, buffer)
950
- response = netius.clients.HTTPClient.to_response(result)
951
- callback and callback(response)
952
-
953
- extra["callback"] = _callback
954
- extra["on_data"] = _on_data
955
- if callback_init:
956
- extra["_on_init"] = _on_init
957
- if callback_open:
958
- extra["on_open"] = _on_open
959
- if callback:
960
- extra["on_close"] = _on_close
961
- if callback_headers:
962
- extra["on_headers"] = _on_headers
963
- if callback_result:
964
- extra["on_result"] = _on_result
965
-
966
- return extra
967
-
968
-
969
- def _cleanup_netius():
970
- global _netius_clients
971
- for netius_client in _netius_clients.values():
972
- netius_client.cleanup()
973
- del _netius_clients
974
-
975
-
976
- def _parse_url(url):
977
- parse = legacy.urlparse(url)
978
- scheme = parse.scheme
979
- secure = scheme == "https"
980
- default = 443 if secure else 80
981
- port = parse.port or default
982
- url = parse.scheme + "://" + parse.hostname + ":" + str(port) + parse.path
983
- if port in (80, 443):
984
- host = parse.hostname
985
- else:
986
- host = parse.hostname + ":" + str(port)
987
- authorization = _authorization(parse.username, parse.password)
988
- params = _params(parse.query)
989
- return (url, scheme, host, authorization, params)
990
-
991
-
992
- def _result(data, info={}, force=False, strict=False):
993
- # tries to retrieve the content type value from the headers
994
- # info and verifies if the current data is JSON encoded, so
995
- # that it gets automatically decoded for such cases
996
- content_type = info.get("Content-Type", None) or ""
997
- is_json = (
998
- util.is_content_type(
999
- content_type, ("application/json", "text/json", "text/javascript")
1000
- )
1001
- or force
1002
- )
1003
-
1004
- # verifies if the current result set is JSON encoded and in
1005
- # case it's decodes it and loads it as JSON otherwise returns
1006
- # the "raw" data to the caller method as expected, note that
1007
- # the strict flag is used to determine if the exception should
1008
- # be re-raised to the upper level in case of value error
1009
- if is_json and legacy.is_bytes(data):
1010
- data = data.decode("utf-8")
1011
- try:
1012
- data = json.loads(data) if is_json else data
1013
- except ValueError:
1014
- if strict:
1015
- raise
1016
- return data
1017
-
1018
-
1019
- def _params(query):
1020
- # creates the dictionary that is going to be used to store the
1021
- # complete information regarding the parameters in query
1022
- params = dict()
1023
-
1024
- # validates that the provided query value is valid and if
1025
- # that's not the case returns the created parameters immediately
1026
- # (empty parameters are returned)
1027
- if not query:
1028
- return params
1029
-
1030
- # splits the query value around the initial parameter separator
1031
- # symbol and iterates over each of them to parse them and create
1032
- # the proper parameters dictionary (of lists)
1033
- query_s = query.split("&")
1034
- for part in query_s:
1035
- parts = part.split("=", 1)
1036
- if len(parts) == 1:
1037
- value = ""
1038
- else:
1039
- value = parts[1]
1040
- key = parts[0]
1041
- key = legacy.unquote_plus(key)
1042
- value = legacy.unquote_plus(value)
1043
- param = params.get(key, [])
1044
- param.append(value)
1045
- params[key] = param
1046
-
1047
- # returns the final parameters dictionary to the caller method
1048
- # so that it may be used as a proper structure representation
1049
- return params
1050
-
1051
-
1052
- def _urlencode(values, as_string=True):
1053
- # creates the list that will hold the final tuple of values
1054
- # (without the unset and invalid values)
1055
- final = []
1056
-
1057
- # verifies if the provided value is a sequence and in case it's
1058
- # not converts it into a sequence (assuming a map)
1059
- is_sequence = isinstance(values, (list, tuple))
1060
- if not is_sequence:
1061
- values = values.items()
1062
-
1063
- # iterates over all the items in the values sequence to
1064
- # try to filter the values that are not valid
1065
- for key, value in values:
1066
- # creates the list that will hold the valid values
1067
- # of the current key in iteration (sanitized values)
1068
- _values = []
1069
-
1070
- # in case the current data type of the key is unicode
1071
- # the value must be converted into a string using the
1072
- # default utf encoding strategy (as defined)
1073
- if type(key) == legacy.UNICODE:
1074
- key = key.encode("utf-8")
1075
-
1076
- # verifies the type of the current value and in case
1077
- # it's sequence based converts it into a list using
1078
- # the conversion method otherwise creates a new list
1079
- # and includes the value in it
1080
- value_t = type(value)
1081
- if value_t in SEQUENCE_TYPES:
1082
- value = list(value)
1083
- else:
1084
- value = [value]
1085
-
1086
- # iterates over all the values in the current sequence
1087
- # and adds the valid values to the sanitized sequence,
1088
- # this includes the conversion from unicode string into
1089
- # a simple string using the default utf encoder
1090
- for _value in value:
1091
- if _value == None:
1092
- continue
1093
- is_string = type(_value) in legacy.STRINGS
1094
- if not is_string:
1095
- _value = str(_value)
1096
- is_unicode = type(_value) == legacy.UNICODE
1097
- if is_unicode:
1098
- _value = _value.encode("utf-8")
1099
- _values.append(_value)
1100
-
1101
- # sets the sanitized list of values as the new value for
1102
- # the key in the final dictionary of values
1103
- final.append((key, _values))
1104
-
1105
- # in case the "as string" flag is not set the ended key to value
1106
- # dictionary should be returned to the called method and not the
1107
- # "default" linear and string based value
1108
- if not as_string:
1109
- return final
1110
-
1111
- # runs the encoding with sequence support on the final map
1112
- # of sanitized values and returns the encoded result to the
1113
- # caller method as the encoded value
1114
- return legacy.urlencode(final, doseq=True)
1115
-
1116
-
1117
- def _quote(values, plus=False, safe="/"):
1118
- method = legacy.quote_plus if plus else legacy.quote
1119
- values = _urlencode(values, as_string=False)
1120
-
1121
- final = dict()
1122
-
1123
- for key, value in values:
1124
- key = method(key, safe=safe)
1125
- value = method(value[0], safe=safe)
1126
- final[key] = value
1127
-
1128
- return final
1129
-
1130
-
1131
- def _authorization(username, password):
1132
- if not username:
1133
- return None
1134
- if not password:
1135
- return None
1136
- payload = "%s:%s" % (username, password)
1137
- payload = legacy.bytes(payload)
1138
- authorization = base64.b64encode(payload)
1139
- authorization = legacy.str(authorization)
1140
- return authorization
1141
-
1142
-
1143
- def _encode_multipart(fields, mime=None, doseq=False):
1144
- mime = mime or "multipart/form-data"
1145
- boundary = _create_boundary(fields, doseq=doseq)
1146
- boundary_b = legacy.bytes(boundary)
1147
-
1148
- buffer = []
1149
-
1150
- for key, values in fields.items():
1151
- is_list = doseq and type(values) == list
1152
- values = values if is_list else [values]
1153
-
1154
- for value in values:
1155
- if value == None:
1156
- continue
1157
-
1158
- if isinstance(value, dict):
1159
- header_l = []
1160
- data = None
1161
- for key, item in value.items():
1162
- if key == "data":
1163
- data = item
1164
- else:
1165
- header_l.append("%s: %s" % (key, item))
1166
- value = data
1167
- header = "\r\n".join(header_l)
1168
- elif isinstance(value, tuple):
1169
- content_type = None
1170
- if len(value) == 2:
1171
- name, contents = value
1172
- else:
1173
- name, content_type, contents = value
1174
- header = 'Content-Disposition: form-data; name="%s"; filename="%s"' % (
1175
- key,
1176
- name,
1177
- )
1178
- if content_type:
1179
- header += "\r\nContent-Type: %s" % content_type
1180
- value = contents
1181
- else:
1182
- header = 'Content-Disposition: form-data; name="%s"' % key
1183
- value = _encode(value)
1184
-
1185
- header = _encode(header)
1186
- value = _encode(value)
1187
-
1188
- buffer.append(b"--" + boundary_b)
1189
- buffer.append(header)
1190
- buffer.append(b"")
1191
- buffer.append(value)
1192
-
1193
- buffer.append(b"--" + boundary_b + b"--")
1194
- buffer.append(b"")
1195
- body = b"\r\n".join(buffer)
1196
- content_type = "%s; boundary=%s" % (mime, boundary)
1197
-
1198
- return content_type, body
1199
-
1200
-
1201
- def _create_boundary(fields, size=32, doseq=False):
1202
- while True:
1203
- base = "".join(random.choice(RANGE) for _value in range(size))
1204
- boundary = "----------" + base
1205
- result = _try_boundary(fields, boundary, doseq=doseq)
1206
- if result:
1207
- break
1208
-
1209
- return boundary
1210
-
1211
-
1212
- def _try_boundary(fields, boundary, doseq=False):
1213
- boundary_b = legacy.bytes(boundary)
1214
-
1215
- for key, values in fields.items():
1216
- is_list = doseq and type(values) == list
1217
- values = values if is_list else [values]
1218
-
1219
- for value in values:
1220
- if isinstance(value, dict):
1221
- name = ""
1222
- value = value.get("data", b"")
1223
- elif isinstance(value, tuple):
1224
- if len(value) == 2:
1225
- name = value[0] or ""
1226
- value = value[1] or b""
1227
- else:
1228
- name = value[0] or ""
1229
- value = value[2] or b""
1230
- else:
1231
- name = ""
1232
- value = _encode(value)
1233
-
1234
- if not key.find(boundary) == -1:
1235
- return False
1236
- if not name.find(boundary) == -1:
1237
- return False
1238
- if not value.find(boundary_b) == -1:
1239
- return False
1240
-
1241
- return True
1242
-
1243
-
1244
- def _is_error(code):
1245
- return code // 100 in (4, 5) if code else True
1246
-
1247
-
1248
- def _encode(value, encoding="utf-8"):
1249
- value_t = type(value)
1250
- if value_t == legacy.BYTES:
1251
- return value
1252
- elif value_t == legacy.UNICODE:
1253
- return value.encode(encoding)
1254
- return legacy.bytes(str(value))
1255
-
1256
-
1257
- class HTTPResponse(object):
1258
- """
1259
- Compatibility object to be used by HTTP libraries that do
1260
- not support the legacy HTTP response object as a return
1261
- for any of their structures.
1262
- """
1263
-
1264
- def __init__(self, data=None, code=200, status=None, headers=None):
1265
- self.data = data
1266
- self.code = code
1267
- self.status = status
1268
- self.headers = headers
1269
-
1270
- def read(self):
1271
- return self.data
1272
-
1273
- def readline(self):
1274
- return self.read()
1275
-
1276
- def close(self):
1277
- pass
1278
-
1279
- def getcode(self):
1280
- return self.code
1281
-
1282
- def info(self):
1283
- return self.headers
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Hive Appier Framework
5
+ # Copyright (c) 2008-2024 Hive Solutions Lda.
6
+ #
7
+ # This file is part of Hive Appier Framework.
8
+ #
9
+ # Hive Appier Framework is free software: you can redistribute it and/or modify
10
+ # it under the terms of the Apache License as published by the Apache
11
+ # Foundation, either version 2.0 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Hive Appier Framework is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # Apache License for more details.
18
+ #
19
+ # You should have received a copy of the Apache License along with
20
+ # Hive Appier Framework. If not, see <http://www.apache.org/licenses/>.
21
+
22
+ """appier.http
23
+
24
+ Unified HTTP helper wrapping requests for synchronous calls.
25
+ Provides thin convenience functions (`get`, `post`, etc.) that
26
+ automatically propagate headers, query strings and timeouts as
27
+ expected by Appier internals. Used by API clients and tests to
28
+ execute outbound requests in a consistent way.
29
+ """
30
+
31
+ __author__ = "João Magalhães <joamag@hive.pt>"
32
+ """ The author(s) of the module """
33
+
34
+ __copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
35
+ """ The copyright for the module """
36
+
37
+ __license__ = "Apache License, Version 2.0"
38
+ """ The license for the module """
39
+
40
+ import os
41
+ import json
42
+ import base64
43
+ import string
44
+ import random
45
+ import logging
46
+ import threading
47
+
48
+ from . import util
49
+ from . import common
50
+ from . import legacy
51
+ from . import typesf
52
+ from . import config
53
+ from . import exceptions
54
+ from . import structures
55
+
56
+ TIMEOUT = 60
57
+ """ The timeout in seconds to be used for the blocking
58
+ operations in the HTTP connection, this value avoid unwanted
59
+ blocking operations to remain open for an infinite time """
60
+
61
+ RANGE = string.ascii_letters + string.digits
62
+ """ The range of characters that are going to be used in
63
+ the generation of the boundary value for the mime """
64
+
65
+ SEQUENCE_TYPES = (list, tuple)
66
+ """ The sequence defining the various types that are
67
+ considered to be sequence based for python """
68
+
69
+ AUTH_ERRORS = (401, 403, 440, 499)
70
+ """ The sequence that defines the various HTTP errors
71
+ considered to be authentication related and for which a
72
+ new authentication try will be performed """
73
+
74
+ ACCESS_LOCK = threading.RLock()
75
+ """ Global access lock used for locking global operations
76
+ that require thread safety under the HTTP infra-structure """
77
+
78
+
79
+ def file_g(path, chunk=40960):
80
+ yield os.path.getsize(path)
81
+ file = open(path, "rb")
82
+ try:
83
+ while True:
84
+ data = file.read(chunk)
85
+ if not data:
86
+ break
87
+ yield data
88
+ finally:
89
+ file.close()
90
+
91
+
92
+ def get_f(*args, **kwargs):
93
+ name = kwargs.pop("name", "default")
94
+ kwargs["handle"] = kwargs.get("handle", True)
95
+ kwargs["redirect"] = kwargs.get("redirect", True)
96
+ data, response = get(*args, **kwargs)
97
+ info = response.info()
98
+ mime = info.get("Content-Type", None)
99
+ file_tuple = util.FileTuple((name, mime, data))
100
+ return typesf.File(file_tuple)
101
+
102
+
103
+ def get(
104
+ url,
105
+ params=None,
106
+ headers=None,
107
+ handle=None,
108
+ silent=None,
109
+ redirect=None,
110
+ timeout=None,
111
+ auth_callback=None,
112
+ **kwargs
113
+ ):
114
+ return _method(
115
+ _get,
116
+ url,
117
+ params=params,
118
+ headers=headers,
119
+ handle=handle,
120
+ silent=silent,
121
+ redirect=redirect,
122
+ timeout=timeout,
123
+ auth_callback=auth_callback,
124
+ **kwargs
125
+ )
126
+
127
+
128
+ def post(
129
+ url,
130
+ params=None,
131
+ data=None,
132
+ data_j=None,
133
+ data_m=None,
134
+ headers=None,
135
+ mime=None,
136
+ handle=None,
137
+ silent=None,
138
+ redirect=None,
139
+ timeout=None,
140
+ auth_callback=None,
141
+ **kwargs
142
+ ):
143
+ return _method(
144
+ _post,
145
+ url,
146
+ params=params,
147
+ data=data,
148
+ data_j=data_j,
149
+ data_m=data_m,
150
+ headers=headers,
151
+ mime=mime,
152
+ handle=handle,
153
+ silent=silent,
154
+ redirect=redirect,
155
+ timeout=timeout,
156
+ auth_callback=auth_callback,
157
+ **kwargs
158
+ )
159
+
160
+
161
+ def put(
162
+ url,
163
+ params=None,
164
+ data=None,
165
+ data_j=None,
166
+ data_m=None,
167
+ headers=None,
168
+ mime=None,
169
+ handle=None,
170
+ silent=None,
171
+ redirect=None,
172
+ timeout=None,
173
+ auth_callback=None,
174
+ **kwargs
175
+ ):
176
+ return _method(
177
+ _put,
178
+ url,
179
+ params=params,
180
+ data=data,
181
+ data_j=data_j,
182
+ data_m=data_m,
183
+ headers=headers,
184
+ mime=mime,
185
+ handle=handle,
186
+ silent=silent,
187
+ redirect=redirect,
188
+ timeout=timeout,
189
+ auth_callback=auth_callback,
190
+ **kwargs
191
+ )
192
+
193
+
194
+ def delete(
195
+ url,
196
+ params=None,
197
+ headers=None,
198
+ handle=None,
199
+ silent=None,
200
+ redirect=None,
201
+ timeout=None,
202
+ auth_callback=None,
203
+ **kwargs
204
+ ):
205
+ return _method(
206
+ _delete,
207
+ url,
208
+ params=params,
209
+ headers=headers,
210
+ handle=handle,
211
+ silent=silent,
212
+ redirect=redirect,
213
+ timeout=timeout,
214
+ auth_callback=auth_callback,
215
+ **kwargs
216
+ )
217
+
218
+
219
+ def patch(
220
+ url,
221
+ params=None,
222
+ data=None,
223
+ data_j=None,
224
+ data_m=None,
225
+ headers=None,
226
+ mime=None,
227
+ handle=None,
228
+ silent=None,
229
+ redirect=None,
230
+ timeout=None,
231
+ auth_callback=None,
232
+ **kwargs
233
+ ):
234
+ return _method(
235
+ _patch,
236
+ url,
237
+ params=params,
238
+ data=data,
239
+ data_j=data_j,
240
+ data_m=data_m,
241
+ headers=headers,
242
+ mime=mime,
243
+ handle=handle,
244
+ silent=silent,
245
+ redirect=redirect,
246
+ timeout=timeout,
247
+ auth_callback=auth_callback,
248
+ **kwargs
249
+ )
250
+
251
+
252
+ def basic_auth(username, password=None):
253
+ if not password:
254
+ password = username
255
+ authorization = _authorization(username, password)
256
+ return "Basic %s" % authorization
257
+
258
+
259
+ def _try_auth(auth_callback, params, headers=None):
260
+ if not auth_callback:
261
+ raise
262
+ if headers == None:
263
+ headers = dict()
264
+ auth_callback(params, headers)
265
+
266
+
267
+ def _method(method, *args, **kwargs):
268
+ try:
269
+ auth_callback = kwargs.pop("auth_callback", None)
270
+ result = method(*args, **kwargs)
271
+ except legacy.HTTPError as error:
272
+ try:
273
+ params = kwargs.get("params", None)
274
+ headers = kwargs.get("headers", None)
275
+ if not error.code in AUTH_ERRORS:
276
+ raise
277
+ _try_auth(auth_callback, params, headers)
278
+ result = method(*args, **kwargs)
279
+ except legacy.HTTPError as error:
280
+ code = error.getcode()
281
+ raise exceptions.HTTPError(error, code)
282
+
283
+ return result
284
+
285
+
286
+ def _get(
287
+ url,
288
+ params=None,
289
+ headers=None,
290
+ handle=None,
291
+ silent=None,
292
+ redirect=None,
293
+ timeout=None,
294
+ **kwargs
295
+ ):
296
+ return _method_empty(
297
+ "GET",
298
+ url,
299
+ params=params,
300
+ headers=headers,
301
+ handle=handle,
302
+ silent=silent,
303
+ redirect=redirect,
304
+ timeout=timeout,
305
+ **kwargs
306
+ )
307
+
308
+
309
+ def _post(
310
+ url,
311
+ params=None,
312
+ data=None,
313
+ data_j=None,
314
+ data_m=None,
315
+ headers=None,
316
+ mime=None,
317
+ handle=None,
318
+ silent=None,
319
+ redirect=None,
320
+ timeout=None,
321
+ **kwargs
322
+ ):
323
+ return _method_payload(
324
+ "POST",
325
+ url,
326
+ params=params,
327
+ data=data,
328
+ data_j=data_j,
329
+ data_m=data_m,
330
+ headers=headers,
331
+ mime=mime,
332
+ handle=handle,
333
+ silent=silent,
334
+ redirect=redirect,
335
+ timeout=timeout,
336
+ **kwargs
337
+ )
338
+
339
+
340
+ def _put(
341
+ url,
342
+ params=None,
343
+ data=None,
344
+ data_j=None,
345
+ data_m=None,
346
+ headers=None,
347
+ mime=None,
348
+ handle=None,
349
+ silent=None,
350
+ redirect=None,
351
+ timeout=None,
352
+ **kwargs
353
+ ):
354
+ return _method_payload(
355
+ "PUT",
356
+ url,
357
+ params=params,
358
+ data=data,
359
+ data_j=data_j,
360
+ data_m=data_m,
361
+ headers=headers,
362
+ mime=mime,
363
+ handle=handle,
364
+ silent=silent,
365
+ redirect=redirect,
366
+ timeout=timeout,
367
+ **kwargs
368
+ )
369
+
370
+
371
+ def _delete(
372
+ url,
373
+ params=None,
374
+ headers=None,
375
+ handle=None,
376
+ silent=None,
377
+ redirect=None,
378
+ timeout=None,
379
+ **kwargs
380
+ ):
381
+ return _method_empty(
382
+ "DELETE",
383
+ url,
384
+ params=params,
385
+ headers=headers,
386
+ handle=handle,
387
+ silent=silent,
388
+ redirect=redirect,
389
+ timeout=timeout,
390
+ **kwargs
391
+ )
392
+
393
+
394
+ def _patch(
395
+ url,
396
+ params=None,
397
+ data=None,
398
+ data_j=None,
399
+ data_m=None,
400
+ headers=None,
401
+ mime=None,
402
+ handle=None,
403
+ silent=None,
404
+ redirect=None,
405
+ timeout=None,
406
+ **kwargs
407
+ ):
408
+ return _method_payload(
409
+ "PATCH",
410
+ url,
411
+ params=params,
412
+ data=data,
413
+ data_j=data_j,
414
+ data_m=data_m,
415
+ headers=headers,
416
+ mime=mime,
417
+ handle=handle,
418
+ silent=silent,
419
+ redirect=redirect,
420
+ timeout=timeout,
421
+ **kwargs
422
+ )
423
+
424
+
425
+ def _method_empty(
426
+ name,
427
+ url,
428
+ params=None,
429
+ headers=None,
430
+ handle=None,
431
+ silent=None,
432
+ redirect=None,
433
+ timeout=None,
434
+ **kwargs
435
+ ):
436
+ if handle == None:
437
+ handle = False
438
+ if silent == None:
439
+ silent = config.conf("HTTP_SILENT", False, cast=bool)
440
+ if redirect == None:
441
+ redirect = config.conf("HTTP_REDIRECT", False, cast=bool)
442
+ if timeout == None:
443
+ timeout = config.conf("HTTP_TIMEOUT", TIMEOUT, cast=int)
444
+ values = params or dict()
445
+
446
+ values_s = " with '%s'" % str(values) if values else ""
447
+ if not silent:
448
+ logging.debug("%s %s%s" % (name, url, values_s))
449
+
450
+ url, scheme, host, authorization, extra = _parse_url(url)
451
+ if extra:
452
+ values.update(extra)
453
+ data = _urlencode(values)
454
+
455
+ headers = dict(headers) if headers else dict()
456
+ if host:
457
+ headers["Host"] = host
458
+ if authorization:
459
+ headers["Authorization"] = "Basic %s" % authorization
460
+ url = url + "?" + data if data else url
461
+ url = str(url)
462
+
463
+ _method_callback(handle, kwargs)
464
+
465
+ # runs the concrete resolution method (taking into account the adapter)
466
+ # providing it with the required parameters for request execution
467
+ file = _resolve(url, name, headers, None, silent, timeout, **kwargs)
468
+
469
+ # verifies if the resulting "file" from the resolution process is either
470
+ # an invalid or tuple value and if that's the case as the request has
471
+ # probably been deferred for asynchronous execution
472
+ if file == None:
473
+ return file
474
+ if isinstance(file, tuple):
475
+ return file
476
+
477
+ try:
478
+ result = file.read()
479
+ finally:
480
+ file.close()
481
+
482
+ code = file.getcode()
483
+ info = file.info()
484
+
485
+ location = info.get("Location", None) if redirect else None
486
+ if location:
487
+ return _redirect(
488
+ location,
489
+ scheme,
490
+ host,
491
+ handle=handle,
492
+ silent=silent,
493
+ redirect=redirect,
494
+ timeout=timeout,
495
+ **kwargs
496
+ )
497
+
498
+ if not silent:
499
+ logging.debug("%s %s returned '%d'" % (name, url, code))
500
+
501
+ result = _result(result, info)
502
+ return (result, file) if handle else result
503
+
504
+
505
+ def _method_payload(
506
+ name,
507
+ url,
508
+ params=None,
509
+ data=None,
510
+ data_j=None,
511
+ data_m=None,
512
+ headers=None,
513
+ mime=None,
514
+ handle=None,
515
+ silent=None,
516
+ redirect=None,
517
+ timeout=None,
518
+ **kwargs
519
+ ):
520
+ if handle == None:
521
+ handle = False
522
+ if silent == None:
523
+ silent = config.conf("HTTP_SILENT", False, cast=bool)
524
+ if redirect == None:
525
+ redirect = config.conf("HTTP_REDIRECT", False, cast=bool)
526
+ if timeout == None:
527
+ timeout = config.conf("HTTP_TIMEOUT", TIMEOUT, cast=int)
528
+ values = params or dict()
529
+
530
+ values_s = " with '%s'" % str(values) if values else ""
531
+ if not silent:
532
+ logging.debug("%s %s%s" % (name, url, values_s))
533
+
534
+ url, scheme, host, authorization, extra = _parse_url(url)
535
+ if extra:
536
+ values.update(extra)
537
+ data_e = _urlencode(values)
538
+
539
+ if not data == None:
540
+ url = url + "?" + data_e if data_e else url
541
+ elif not data_j == None:
542
+ data = json.dumps(data_j)
543
+ url = url + "?" + data_e if data_e else url
544
+ mime = mime or "application/json"
545
+ elif not data_m == None:
546
+ url = url + "?" + data_e if data_e else url
547
+ content_type, data = _encode_multipart(data_m, mime=mime, doseq=True)
548
+ mime = content_type
549
+ elif data_e:
550
+ data = data_e
551
+ mime = mime or "application/x-www-form-urlencoded"
552
+
553
+ if legacy.is_unicode(data):
554
+ data = legacy.bytes(data, force=True)
555
+
556
+ if not data:
557
+ length = 0
558
+ elif legacy.is_bytes(data):
559
+ length = len(data)
560
+ else:
561
+ length = -1
562
+
563
+ headers = dict(headers) if headers else dict()
564
+ if not length == -1:
565
+ headers["Content-Length"] = str(length)
566
+ if mime:
567
+ headers["Content-Type"] = mime
568
+ if host:
569
+ headers["Host"] = host
570
+ if authorization:
571
+ headers["Authorization"] = "Basic %s" % authorization
572
+ url = str(url)
573
+
574
+ _method_callback(handle, kwargs)
575
+ file = _resolve(url, name, headers, data, silent, timeout, **kwargs)
576
+ if file == None:
577
+ return file
578
+
579
+ try:
580
+ result = file.read()
581
+ finally:
582
+ file.close()
583
+
584
+ code = file.getcode()
585
+ info = file.info()
586
+
587
+ location = info.get("Location", None) if redirect else None
588
+ if location:
589
+ return _redirect(
590
+ location,
591
+ scheme,
592
+ host,
593
+ handle=handle,
594
+ silent=silent,
595
+ redirect=redirect,
596
+ timeout=timeout,
597
+ )
598
+
599
+ if not silent:
600
+ logging.debug("%s %s returned '%d'" % (name, url, code))
601
+
602
+ result = _result(result, info)
603
+ return (result, file) if handle else result
604
+
605
+
606
+ def _method_callback(handle, kwargs):
607
+ # tries to determine if a callback value has been registered
608
+ # in the set of keyword argument and if that's not the case
609
+ # returns immediately (nothing to be done)
610
+ callback = kwargs.get("callback", None)
611
+ if not callback:
612
+ return
613
+
614
+ def callback_wrap(file):
615
+ # determines if the received file is valid (no error)
616
+ # or if instead there's an error with the connection
617
+ # and an invalid/unset value has been provided
618
+ if file:
619
+ info = file.info()
620
+ try:
621
+ result = file.read()
622
+ finally:
623
+ file.close()
624
+ result = _result(result, info)
625
+ else:
626
+ result = None
627
+
628
+ # taking into account the handle flag determines the
629
+ # kind of result structure that should be "returned"
630
+ result = (result, file) if handle else (result,)
631
+ callback(*result)
632
+
633
+ # sets the "new" callback clojure in the set of keyword
634
+ # based arguments for the calling of the HTTP handler method
635
+ # so that this new callback is called instead of the original
636
+ kwargs["callback"] = callback_wrap
637
+
638
+
639
+ def _redirect(
640
+ location,
641
+ scheme,
642
+ host,
643
+ handle=None,
644
+ silent=None,
645
+ redirect=None,
646
+ timeout=None,
647
+ **kwargs
648
+ ):
649
+ is_relative = location.startswith("/")
650
+ if is_relative:
651
+ location = scheme + "://" + host + location
652
+ logging.debug("Redirecting to %s" % location)
653
+ return get(
654
+ location,
655
+ handle=handle,
656
+ silent=silent,
657
+ redirect=redirect,
658
+ timeout=timeout,
659
+ **kwargs
660
+ )
661
+
662
+
663
+ def _resolve(*args, **kwargs):
664
+ # obtains the reference to the global set of variables, so
665
+ # that it's possible to obtain the proper resolver method
666
+ # according to the requested client
667
+ _global = globals()
668
+
669
+ # tries to retrieve the global configuration values that
670
+ # will condition the way the request is going to be performed
671
+ client = config.conf("HTTP_CLIENT", "netius")
672
+ reuse = config.conf("HTTP_REUSE", True, cast=bool)
673
+
674
+ # tries to determine the set of configurations requested on
675
+ # a request basis (not global) these have priority when
676
+ # compared with the global configuration ones
677
+ client = kwargs.pop("client", client)
678
+ reuse = kwargs.pop("reuse", reuse)
679
+
680
+ # sets the value for connection re-usage so that a connection
681
+ # pool is used if request, otherwise one connection per request
682
+ # is going to be used (at the expense of resources)
683
+ kwargs["reuse"] = reuse
684
+
685
+ # tries to retrieve the reference to the resolve method for the
686
+ # current client and then runs it, retrieve then the final result,
687
+ # note that the result structure may be engine dependent
688
+ resolver = _global.get("_resolve_" + client, _resolve_legacy)
689
+ try:
690
+ result = resolver(*args, **kwargs)
691
+ except ImportError:
692
+ result = _resolve_legacy(*args, **kwargs)
693
+ return result
694
+
695
+
696
+ def _resolve_legacy(url, method, headers, data, silent, timeout, **kwargs):
697
+ is_generator = not data == None and legacy.is_generator(data)
698
+ if is_generator:
699
+ next(data)
700
+ data = b"".join(data)
701
+ is_file = hasattr(data, "tell")
702
+ if is_file:
703
+ data = data.read()
704
+ opener = legacy.build_opener(legacy.HTTPHandler)
705
+ request = legacy.Request(url, data=data, headers=headers)
706
+ request.get_method = lambda: method
707
+ return opener.open(request, timeout=timeout)
708
+
709
+
710
+ def _resolve_requests(url, method, headers, data, silent, timeout, **kwargs):
711
+ util.ensure_pip("requests")
712
+ import requests
713
+
714
+ global _requests_session
715
+
716
+ # retrieves the various dynamic parameters for the HTTP client
717
+ # usage under the requests infra-structure
718
+ reuse = kwargs.get("reuse", True)
719
+ connections = kwargs.get("connections", 256)
720
+
721
+ # verifies if the provided data is a generator, assumes that if the
722
+ # data is not invalid and is of type generator then it's a generator
723
+ # and then if that's the case encapsulates this size based generator
724
+ # into a generator based file-like object so that it can be used inside
725
+ # the request infra-structure (as it accepts only file objects)
726
+ is_generator = not data == None and legacy.is_generator(data)
727
+ if is_generator:
728
+ data = structures.GeneratorFile(data)
729
+
730
+ # verifies if the session for the requests infra-structure is
731
+ # already created and if that's not the case and the re-use
732
+ # flag is sets creates a new session for the requested settings
733
+ registered = "_requests_session" in globals()
734
+ if not registered and reuse:
735
+ _requests_session = requests.Session()
736
+ adapter = requests.adapters.HTTPAdapter(
737
+ pool_connections=connections, pool_maxsize=connections
738
+ )
739
+ _requests_session.mount("", adapter)
740
+
741
+ # determines the based object from which the concrete methods
742
+ # are going to be loaded by inspecting the re-use flag
743
+ if reuse:
744
+ base = _requests_session
745
+ else:
746
+ base = requests
747
+
748
+ # converts the string based method value into a lower cased value
749
+ # and then uses it to retrieve the method object method (callable)
750
+ # that is going to be called to perform the request
751
+ method = method.lower()
752
+ caller = getattr(base, method)
753
+
754
+ # runs the caller method (according to selected method) and waits for
755
+ # the result object converting it then to the target response object
756
+ result = caller(url, headers=headers, data=data, timeout=timeout)
757
+ response = HTTPResponse(
758
+ data=result.content, code=result.status_code, headers=result.headers
759
+ )
760
+
761
+ # retrieves the response code of the created response and verifies if
762
+ # it represent an error, if that's the case raised an error exception
763
+ # to the upper layers to break the current execution logic properly
764
+ code = response.getcode()
765
+ is_error = _is_error(code)
766
+ if is_error:
767
+ raise legacy.HTTPError(url, code, "HTTP retrieval problem", None, response)
768
+
769
+ # returns the final response object to the caller method, this object
770
+ # should comply with the proper upper layers structure
771
+ return response
772
+
773
+
774
+ def _resolve_netius(url, method, headers, data, silent, timeout, **kwargs):
775
+ util.ensure_pip("netius")
776
+ import netius.clients
777
+
778
+ # determines the final value of the silent flag taking into
779
+ # account if the current infra-structure is not running under
780
+ # a development environment
781
+ silent = silent or False
782
+ silent |= not common.is_devel()
783
+
784
+ # converts the provided dictionary of headers into a new map to
785
+ # allow any re-writing of values, valuable for a re-connect
786
+ headers = dict(headers)
787
+
788
+ # tries to determine the proper level of verbosity to be used by
789
+ # the client, for that the system tries to determine if the current
790
+ # execution environment is a development one (verbose)
791
+ level = logging.CRITICAL if silent else logging.DEBUG
792
+
793
+ # retrieves the various dynamic parameters for the HTTP client
794
+ # usage under the netius infra-structure
795
+ retry = kwargs.get("retry", 1)
796
+ reuse = kwargs.get("reuse", True)
797
+ level = kwargs.get("level", level)
798
+ asynchronous = kwargs.get("async", False)
799
+ asynchronous = kwargs.get("asynchronous", asynchronous)
800
+ use_file = kwargs.get("use_file", False)
801
+ callback = kwargs.get("callback", None)
802
+ callback_init = kwargs.get("callback_init", None)
803
+ callback_open = kwargs.get("callback_open", None)
804
+ callback_headers = kwargs.get("callback_headers", None)
805
+ callback_data = kwargs.get("callback_data", None)
806
+ callback_result = kwargs.get("callback_result", None)
807
+
808
+ # re-calculates the retry and re-use flags taking into account
809
+ # the async flag, if the execution mode is async we don't want
810
+ # to re-use the HTTP client as it would create issues
811
+ retry, reuse = (0, False) if asynchronous else (retry, reuse)
812
+
813
+ # creates the proper set of extra parameters to be sent to the
814
+ # HTTP client taking into account a possible async method request
815
+ extra = (
816
+ _async_netius(
817
+ callback=callback,
818
+ callback_init=callback_init,
819
+ callback_open=callback_open,
820
+ callback_headers=callback_headers,
821
+ callback_data=callback_data,
822
+ callback_result=callback_result,
823
+ )
824
+ if asynchronous
825
+ else dict(
826
+ on_init=lambda c: callback_init and callback_init(c),
827
+ on_open=lambda c: callback_open and callback_open(c),
828
+ on_headers=lambda c, p: callback_headers and callback_headers(p.headers),
829
+ on_data=lambda c, p, d: callback_data and callback_data(d),
830
+ on_result=lambda c, p, r: callback_result and callback_result(r),
831
+ )
832
+ )
833
+
834
+ # verifies if client re-usage must be enforced and if that's the
835
+ # case the global client object is requested (singleton) otherwise
836
+ # the client should be created inside the HTTP client static method
837
+ http_client = _client_netius(level=level) if reuse else None
838
+ result = netius.clients.HTTPClient.method_s(
839
+ method,
840
+ url,
841
+ headers=headers,
842
+ data=data,
843
+ asynchronous=asynchronous,
844
+ timeout=timeout,
845
+ use_file=use_file,
846
+ http_client=http_client,
847
+ level=level,
848
+ **extra
849
+ )
850
+
851
+ # if the async mode is defined the result (tuple) is returned immediately
852
+ # as the processing will be taking place latter (on callback)
853
+ if asynchronous:
854
+ return result
855
+
856
+ # tries to retrieve any possible error coming from the result object
857
+ # if this happens it means an exception has been raised internally and
858
+ # the error should be handled in a proper manner, if the error is related
859
+ # to a closed connection a retry may be performed to try to re-establish
860
+ # the connection (allows for reconnection in connection pool)
861
+ error = result.get("error", None)
862
+ if error == "closed" and retry > 0:
863
+ kwargs["retry"] = retry - 1
864
+ return _resolve_netius(url, method, headers, data, silent, timeout, **kwargs)
865
+
866
+ # converts the netius specific result map into a response compatible
867
+ # object (equivalent to the urllib one) to be used by the upper layers
868
+ # under an equivalent and compatible approach note that this conversion
869
+ # may raise an exception in case the result represent an error
870
+ response = netius.clients.HTTPClient.to_response(result)
871
+
872
+ # retrieves the response code of the created response and verifies if
873
+ # it represent an error, if that's the case raised an error exception
874
+ # to the upper layers to break the current execution logic properly
875
+ code = response.getcode()
876
+ is_error = _is_error(code)
877
+ if is_error:
878
+ raise legacy.HTTPError(url, code, "HTTP retrieval problem", None, response)
879
+
880
+ # returns the final response object to the upper layers, this object
881
+ # may be used freely under the compatibility interface it provides
882
+ return response
883
+
884
+
885
+ def _client_netius(level=logging.CRITICAL):
886
+ import netius.clients
887
+
888
+ global _netius_clients
889
+
890
+ # retrieves the reference to the current thread and uses the value
891
+ # to retrieve the thread identifier (TID) for it, to be used in the
892
+ # identification of the client resource associated with it
893
+ tid = threading.current_thread().ident
894
+
895
+ # acquires the global HTTP lock and executes a series of validation
896
+ # and initialization of the netius client infra-structure, this
897
+ # operations required thread safety
898
+ ACCESS_LOCK.acquire()
899
+ try:
900
+ registered = "_netius_clients" in globals()
901
+ _netius_clients = _netius_clients if registered else dict()
902
+ netius_client = _netius_clients.get(tid, None)
903
+ finally:
904
+ ACCESS_LOCK.release()
905
+
906
+ # in case a previously created netius client has been retrieved
907
+ # returns it to the caller method for proper re-usage
908
+ if netius_client:
909
+ return netius_client
910
+
911
+ # creates the "new" HTTP client for the current thread and registers
912
+ # it under the netius client structure so that it may be re-used
913
+ netius_client = netius.clients.HTTPClient(auto_release=False)
914
+ _netius_clients[tid] = netius_client
915
+
916
+ # in case this is the first registration of the dictionary a new on
917
+ # exit callback is registered to cleanup the netius infra-structure
918
+ # then the final client is returned to the caller of the method
919
+ if not registered:
920
+ common.base().on_exit(_cleanup_netius)
921
+ return netius_client
922
+
923
+
924
+ def _async_netius(
925
+ callback=None,
926
+ callback_init=None,
927
+ callback_open=None,
928
+ callback_headers=None,
929
+ callback_data=None,
930
+ callback_result=None,
931
+ ):
932
+ import netius.clients
933
+
934
+ buffer = []
935
+ extra = dict()
936
+
937
+ def _on_init(protocol):
938
+ callback_init and callback_init(protocol)
939
+
940
+ def _on_open(protocol):
941
+ callback_open and callback_open(protocol)
942
+
943
+ def _on_close(protocol):
944
+ callback and callback(None)
945
+
946
+ def _on_headers(protocol, parser):
947
+ callback_headers and callback_headers(parser.headers)
948
+
949
+ def _on_data(protocol, parser, data):
950
+ data = data
951
+ data and buffer.append(data)
952
+ callback_data and callback_data(data)
953
+
954
+ def _on_result(protocol, parser, result):
955
+ callback_result and callback_result(result)
956
+
957
+ def _callback(protocol, parser, message):
958
+ result = netius.clients.HTTPProtocol.set_request(parser, buffer)
959
+ response = netius.clients.HTTPClient.to_response(result)
960
+ callback and callback(response)
961
+
962
+ extra["callback"] = _callback
963
+ extra["on_data"] = _on_data
964
+ if callback_init:
965
+ extra["_on_init"] = _on_init
966
+ if callback_open:
967
+ extra["on_open"] = _on_open
968
+ if callback:
969
+ extra["on_close"] = _on_close
970
+ if callback_headers:
971
+ extra["on_headers"] = _on_headers
972
+ if callback_result:
973
+ extra["on_result"] = _on_result
974
+
975
+ return extra
976
+
977
+
978
+ def _cleanup_netius():
979
+ global _netius_clients
980
+ for netius_client in _netius_clients.values():
981
+ netius_client.cleanup()
982
+ del _netius_clients
983
+
984
+
985
+ def _parse_url(url):
986
+ parse = legacy.urlparse(url)
987
+ scheme = parse.scheme
988
+ secure = scheme == "https"
989
+ default = 443 if secure else 80
990
+ port = parse.port or default
991
+ url = parse.scheme + "://" + parse.hostname + ":" + str(port) + parse.path
992
+ if port in (80, 443):
993
+ host = parse.hostname
994
+ else:
995
+ host = parse.hostname + ":" + str(port)
996
+ authorization = _authorization(parse.username, parse.password)
997
+ params = _params(parse.query)
998
+ return (url, scheme, host, authorization, params)
999
+
1000
+
1001
+ def _result(data, info={}, force=False, strict=False):
1002
+ # tries to retrieve the content type value from the headers
1003
+ # info and verifies if the current data is JSON encoded, so
1004
+ # that it gets automatically decoded for such cases
1005
+ content_type = info.get("Content-Type", None) or ""
1006
+ is_json = (
1007
+ util.is_content_type(
1008
+ content_type, ("application/json", "text/json", "text/javascript")
1009
+ )
1010
+ or force
1011
+ )
1012
+
1013
+ # verifies if the current result set is JSON encoded and in
1014
+ # case it's decodes it and loads it as JSON otherwise returns
1015
+ # the "raw" data to the caller method as expected, note that
1016
+ # the strict flag is used to determine if the exception should
1017
+ # be re-raised to the upper level in case of value error
1018
+ if is_json and legacy.is_bytes(data):
1019
+ data = data.decode("utf-8")
1020
+ try:
1021
+ data = json.loads(data) if is_json else data
1022
+ except ValueError:
1023
+ if strict:
1024
+ raise
1025
+ return data
1026
+
1027
+
1028
+ def _params(query):
1029
+ # creates the dictionary that is going to be used to store the
1030
+ # complete information regarding the parameters in query
1031
+ params = dict()
1032
+
1033
+ # validates that the provided query value is valid and if
1034
+ # that's not the case returns the created parameters immediately
1035
+ # (empty parameters are returned)
1036
+ if not query:
1037
+ return params
1038
+
1039
+ # splits the query value around the initial parameter separator
1040
+ # symbol and iterates over each of them to parse them and create
1041
+ # the proper parameters dictionary (of lists)
1042
+ query_s = query.split("&")
1043
+ for part in query_s:
1044
+ parts = part.split("=", 1)
1045
+ if len(parts) == 1:
1046
+ value = ""
1047
+ else:
1048
+ value = parts[1]
1049
+ key = parts[0]
1050
+ key = legacy.unquote_plus(key)
1051
+ value = legacy.unquote_plus(value)
1052
+ param = params.get(key, [])
1053
+ param.append(value)
1054
+ params[key] = param
1055
+
1056
+ # returns the final parameters dictionary to the caller method
1057
+ # so that it may be used as a proper structure representation
1058
+ return params
1059
+
1060
+
1061
+ def _urlencode(values, as_string=True):
1062
+ # creates the list that will hold the final tuple of values
1063
+ # (without the unset and invalid values)
1064
+ final = []
1065
+
1066
+ # verifies if the provided value is a sequence and in case it's
1067
+ # not converts it into a sequence (assuming a map)
1068
+ is_sequence = isinstance(values, (list, tuple))
1069
+ if not is_sequence:
1070
+ values = values.items()
1071
+
1072
+ # iterates over all the items in the values sequence to
1073
+ # try to filter the values that are not valid
1074
+ for key, value in values:
1075
+ # creates the list that will hold the valid values
1076
+ # of the current key in iteration (sanitized values)
1077
+ _values = []
1078
+
1079
+ # in case the current data type of the key is unicode
1080
+ # the value must be converted into a string using the
1081
+ # default utf encoding strategy (as defined)
1082
+ if type(key) == legacy.UNICODE:
1083
+ key = key.encode("utf-8")
1084
+
1085
+ # verifies the type of the current value and in case
1086
+ # it's sequence based converts it into a list using
1087
+ # the conversion method otherwise creates a new list
1088
+ # and includes the value in it
1089
+ value_t = type(value)
1090
+ if value_t in SEQUENCE_TYPES:
1091
+ value = list(value)
1092
+ else:
1093
+ value = [value]
1094
+
1095
+ # iterates over all the values in the current sequence
1096
+ # and adds the valid values to the sanitized sequence,
1097
+ # this includes the conversion from unicode string into
1098
+ # a simple string using the default utf encoder
1099
+ for _value in value:
1100
+ if _value == None:
1101
+ continue
1102
+ is_string = type(_value) in legacy.STRINGS
1103
+ if not is_string:
1104
+ _value = str(_value)
1105
+ is_unicode = type(_value) == legacy.UNICODE
1106
+ if is_unicode:
1107
+ _value = _value.encode("utf-8")
1108
+ _values.append(_value)
1109
+
1110
+ # sets the sanitized list of values as the new value for
1111
+ # the key in the final dictionary of values
1112
+ final.append((key, _values))
1113
+
1114
+ # in case the "as string" flag is not set the ended key to value
1115
+ # dictionary should be returned to the called method and not the
1116
+ # "default" linear and string based value
1117
+ if not as_string:
1118
+ return final
1119
+
1120
+ # runs the encoding with sequence support on the final map
1121
+ # of sanitized values and returns the encoded result to the
1122
+ # caller method as the encoded value
1123
+ return legacy.urlencode(final, doseq=True)
1124
+
1125
+
1126
+ def _quote(values, plus=False, safe="/"):
1127
+ method = legacy.quote_plus if plus else legacy.quote
1128
+ values = _urlencode(values, as_string=False)
1129
+
1130
+ final = dict()
1131
+
1132
+ for key, value in values:
1133
+ key = method(key, safe=safe)
1134
+ value = method(value[0], safe=safe)
1135
+ final[key] = value
1136
+
1137
+ return final
1138
+
1139
+
1140
+ def _authorization(username, password):
1141
+ if not username:
1142
+ return None
1143
+ if not password:
1144
+ return None
1145
+ payload = "%s:%s" % (username, password)
1146
+ payload = legacy.bytes(payload)
1147
+ authorization = base64.b64encode(payload)
1148
+ authorization = legacy.str(authorization)
1149
+ return authorization
1150
+
1151
+
1152
+ def _encode_multipart(fields, mime=None, doseq=False):
1153
+ mime = mime or "multipart/form-data"
1154
+ boundary = _create_boundary(fields, doseq=doseq)
1155
+ boundary_b = legacy.bytes(boundary)
1156
+
1157
+ buffer = []
1158
+
1159
+ for key, values in fields.items():
1160
+ is_list = doseq and type(values) == list
1161
+ values = values if is_list else [values]
1162
+
1163
+ for value in values:
1164
+ if value == None:
1165
+ continue
1166
+
1167
+ if isinstance(value, dict):
1168
+ header_l = []
1169
+ data = None
1170
+ for key, item in value.items():
1171
+ if key == "data":
1172
+ data = item
1173
+ else:
1174
+ header_l.append("%s: %s" % (key, item))
1175
+ value = data
1176
+ header = "\r\n".join(header_l)
1177
+ elif isinstance(value, tuple):
1178
+ content_type = None
1179
+ if len(value) == 2:
1180
+ name, contents = value
1181
+ else:
1182
+ name, content_type, contents = value
1183
+ header = 'Content-Disposition: form-data; name="%s"; filename="%s"' % (
1184
+ key,
1185
+ name,
1186
+ )
1187
+ if content_type:
1188
+ header += "\r\nContent-Type: %s" % content_type
1189
+ value = contents
1190
+ else:
1191
+ header = 'Content-Disposition: form-data; name="%s"' % key
1192
+ value = _encode(value)
1193
+
1194
+ header = _encode(header)
1195
+ value = _encode(value)
1196
+
1197
+ buffer.append(b"--" + boundary_b)
1198
+ buffer.append(header)
1199
+ buffer.append(b"")
1200
+ buffer.append(value)
1201
+
1202
+ buffer.append(b"--" + boundary_b + b"--")
1203
+ buffer.append(b"")
1204
+ body = b"\r\n".join(buffer)
1205
+ content_type = "%s; boundary=%s" % (mime, boundary)
1206
+
1207
+ return content_type, body
1208
+
1209
+
1210
+ def _create_boundary(fields, size=32, doseq=False):
1211
+ while True:
1212
+ base = "".join(random.choice(RANGE) for _value in range(size))
1213
+ boundary = "----------" + base
1214
+ result = _try_boundary(fields, boundary, doseq=doseq)
1215
+ if result:
1216
+ break
1217
+
1218
+ return boundary
1219
+
1220
+
1221
+ def _try_boundary(fields, boundary, doseq=False):
1222
+ boundary_b = legacy.bytes(boundary)
1223
+
1224
+ for key, values in fields.items():
1225
+ is_list = doseq and type(values) == list
1226
+ values = values if is_list else [values]
1227
+
1228
+ for value in values:
1229
+ if isinstance(value, dict):
1230
+ name = ""
1231
+ value = value.get("data", b"")
1232
+ elif isinstance(value, tuple):
1233
+ if len(value) == 2:
1234
+ name = value[0] or ""
1235
+ value = value[1] or b""
1236
+ else:
1237
+ name = value[0] or ""
1238
+ value = value[2] or b""
1239
+ else:
1240
+ name = ""
1241
+ value = _encode(value)
1242
+
1243
+ if not key.find(boundary) == -1:
1244
+ return False
1245
+ if not name.find(boundary) == -1:
1246
+ return False
1247
+ if not value.find(boundary_b) == -1:
1248
+ return False
1249
+
1250
+ return True
1251
+
1252
+
1253
+ def _is_error(code):
1254
+ return code // 100 in (4, 5) if code else True
1255
+
1256
+
1257
+ def _encode(value, encoding="utf-8"):
1258
+ value_t = type(value)
1259
+ if value_t == legacy.BYTES:
1260
+ return value
1261
+ elif value_t == legacy.UNICODE:
1262
+ return value.encode(encoding)
1263
+ return legacy.bytes(str(value))
1264
+
1265
+
1266
+ class HTTPResponse(object):
1267
+ """
1268
+ Compatibility object to be used by HTTP libraries that do
1269
+ not support the legacy HTTP response object as a return
1270
+ for any of their structures.
1271
+ """
1272
+
1273
+ def __init__(self, data=None, code=200, status=None, headers=None):
1274
+ self.data = data
1275
+ self.code = code
1276
+ self.status = status
1277
+ self.headers = headers
1278
+
1279
+ def read(self):
1280
+ return self.data
1281
+
1282
+ def readline(self):
1283
+ return self.read()
1284
+
1285
+ def close(self):
1286
+ pass
1287
+
1288
+ def getcode(self):
1289
+ return self.code
1290
+
1291
+ def info(self):
1292
+ return self.headers