universal-common-net-http 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1134 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import gzip
6
+ import http.client
7
+ import http.cookiejar
8
+ import json
9
+ import socket
10
+ import ssl
11
+ import threading
12
+ import time
13
+ import zlib
14
+ from typing import Any, AsyncIterator, Callable, Iterable, Optional, Union
15
+ from urllib.parse import urlencode, urljoin, urlparse
16
+
17
+ class HttpMethod:
18
+ GET = "GET"
19
+ POST = "POST"
20
+ PUT = "PUT"
21
+ DELETE = "DELETE"
22
+ HEAD = "HEAD"
23
+ OPTIONS = "OPTIONS"
24
+ TRACE = "TRACE"
25
+ PATCH = "PATCH"
26
+
27
+ class HttpStatusCode:
28
+ CONTINUE = 100
29
+ SWITCHING_PROTOCOLS = 101
30
+ PROCESSING = 102
31
+ EARLY_HINTS = 103
32
+ OK = 200
33
+ CREATED = 201
34
+ ACCEPTED = 202
35
+ NON_AUTHORITATIVE_INFORMATION = 203
36
+ NO_CONTENT = 204
37
+ RESET_CONTENT = 205
38
+ PARTIAL_CONTENT = 206
39
+ MULTI_STATUS = 207
40
+ ALREADY_REPORTED = 208
41
+ IM_USED = 226
42
+ MULTIPLE_CHOICES = 300
43
+ AMBIGUOUS = 300
44
+ MOVED_PERMANENTLY = 301
45
+ MOVED = 301
46
+ FOUND = 302
47
+ REDIRECT = 302
48
+ SEE_OTHER = 303
49
+ REDIRECT_METHOD = 303
50
+ NOT_MODIFIED = 304
51
+ USE_PROXY = 305
52
+ UNUSED = 306
53
+ TEMPORARY_REDIRECT = 307
54
+ REDIRECT_KEEP_VERB = 307
55
+ PERMANENT_REDIRECT = 308
56
+ BAD_REQUEST = 400
57
+ UNAUTHORIZED = 401
58
+ PAYMENT_REQUIRED = 402
59
+ FORBIDDEN = 403
60
+ NOT_FOUND = 404
61
+ METHOD_NOT_ALLOWED = 405
62
+ NOT_ACCEPTABLE = 406
63
+ PROXY_AUTHENTICATION_REQUIRED = 407
64
+ REQUEST_TIMEOUT = 408
65
+ CONFLICT = 409
66
+ GONE = 410
67
+ LENGTH_REQUIRED = 411
68
+ PRECONDITION_FAILED = 412
69
+ REQUEST_ENTITY_TOO_LARGE = 413
70
+ REQUEST_URI_TOO_LONG = 414
71
+ UNSUPPORTED_MEDIA_TYPE = 415
72
+ REQUESTED_RANGE_NOT_SATISFIABLE = 416
73
+ EXPECTATION_FAILED = 417
74
+ IM_A_TEAPOT = 418
75
+ MISDIRECTED_REQUEST = 421
76
+ UNPROCESSABLE_ENTITY = 422
77
+ LOCKED = 423
78
+ FAILED_DEPENDENCY = 424
79
+ TOO_EARLY = 425
80
+ UPGRADE_REQUIRED = 426
81
+ PRECONDITION_REQUIRED = 428
82
+ TOO_MANY_REQUESTS = 429
83
+ REQUEST_HEADER_FIELDS_TOO_LARGE = 431
84
+ UNAVAILABLE_FOR_LEGAL_REASONS = 451
85
+ INTERNAL_SERVER_ERROR = 500
86
+ NOT_IMPLEMENTED = 501
87
+ BAD_GATEWAY = 502
88
+ SERVICE_UNAVAILABLE = 503
89
+ GATEWAY_TIMEOUT = 504
90
+ HTTP_VERSION_NOT_SUPPORTED = 505
91
+ VARIANT_ALSO_NEGOTIATES = 506
92
+ INSUFFICIENT_STORAGE = 507
93
+ LOOP_DETECTED = 508
94
+ NOT_EXTENDED = 510
95
+ NETWORK_AUTHENTICATION_REQUIRED = 511
96
+
97
+ class HttpVersion:
98
+ """HTTP protocol versions recognized by HttpRequestMessage.version.
99
+
100
+ Only HTTP/1.0 and HTTP/1.1 are actually sendable: Python's standard library
101
+ (http.client) implements nothing above HTTP/1.1. HTTP/2 requires binary framing,
102
+ HPACK header compression, and multiplexed stream state (see the third-party 'h2'
103
+ package); HTTP/3 additionally requires QUIC over UDP (see 'aioquic'). The 2.0/3.0
104
+ constants exist so callers can refer to them symbolically; using them raises
105
+ NotImplementedError at send time rather than silently downgrading or hanging.
106
+ """
107
+
108
+ HTTP_1_0 = "1.0"
109
+ HTTP_1_1 = "1.1"
110
+ HTTP_2_0 = "2.0"
111
+ HTTP_3_0 = "3.0"
112
+
113
+ _SUPPORTED_HTTP_VERSIONS = {
114
+ HttpVersion.HTTP_1_0: (10, "HTTP/1.0"),
115
+ HttpVersion.HTTP_1_1: (11, "HTTP/1.1"),
116
+ }
117
+
118
+ class HttpCompletionOption:
119
+ """Mirrors System.Net.Http.HttpCompletionOption."""
120
+
121
+ RESPONSE_CONTENT_READ = "response_content_read"
122
+ RESPONSE_HEADERS_READ = "response_headers_read"
123
+
124
+ class HttpHeaders:
125
+ """Case-insensitive, multi-value header collection, matching the semantics of System.Net.Http.Headers.HttpHeaders."""
126
+
127
+ def __init__(self, items: Optional[Iterable[tuple[str, str]]] = None):
128
+ self._entries: list[tuple[str, str]] = []
129
+ if items:
130
+ self.update(items)
131
+
132
+ @staticmethod
133
+ def _normalize(name: str) -> str:
134
+ return name.lower()
135
+
136
+ def add(self, name: str, value: str):
137
+ self._entries.append((name, value))
138
+
139
+ def get_all(self, name: str) -> list[str]:
140
+ key = self._normalize(name)
141
+ return [value for entry_name, value in self._entries if self._normalize(entry_name) == key]
142
+
143
+ def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
144
+ values = self.get_all(name)
145
+ return values[0] if values else default
146
+
147
+ def remove(self, name: str):
148
+ key = self._normalize(name)
149
+ self._entries = [entry for entry in self._entries if self._normalize(entry[0]) != key]
150
+
151
+ def pop(self, name: str, default: Optional[str] = None) -> Optional[str]:
152
+ values = self.get_all(name)
153
+ self.remove(name)
154
+ return values[0] if values else default
155
+
156
+ def update(self, other):
157
+ if other is None:
158
+ return
159
+ pairs = other.items() if hasattr(other, "items") else other
160
+ for name, value in pairs:
161
+ self.add(name, value)
162
+
163
+ def items(self) -> list[tuple[str, str]]:
164
+ return list(self._entries)
165
+
166
+ def keys(self) -> list[str]:
167
+ return list(self)
168
+
169
+ def __setitem__(self, name: str, value: str):
170
+ self.remove(name)
171
+ self.add(name, value)
172
+
173
+ def __getitem__(self, name: str) -> str:
174
+ values = self.get_all(name)
175
+ if not values:
176
+ raise KeyError(name)
177
+ return values[0]
178
+
179
+ def __delitem__(self, name: str):
180
+ self.remove(name)
181
+
182
+ def __contains__(self, name: str) -> bool:
183
+ return bool(self.get_all(name))
184
+
185
+ def __iter__(self):
186
+ seen = []
187
+ for name, _ in self._entries:
188
+ key = self._normalize(name)
189
+ if key not in seen:
190
+ seen.append(key)
191
+ yield name
192
+
193
+ def __len__(self) -> int:
194
+ return len(list(iter(self)))
195
+
196
+ def __repr__(self) -> str:
197
+ return f"HttpHeaders({self._entries!r})"
198
+
199
+ def _decompress_content(data: bytes, content_encoding: Optional[str]) -> bytes:
200
+ if not content_encoding:
201
+ return data
202
+
203
+ encoding = content_encoding.lower()
204
+ if encoding == "gzip":
205
+ return gzip.decompress(data)
206
+ if encoding == "deflate":
207
+ try:
208
+ return zlib.decompress(data)
209
+ except zlib.error:
210
+ return zlib.decompress(data, -zlib.MAX_WBITS)
211
+ return data
212
+
213
+ def _read_bounded(response: http.client.HTTPResponse, max_size: Optional[int]) -> bytes:
214
+ if max_size is None:
215
+ return response.read()
216
+
217
+ chunks = []
218
+ total = 0
219
+ while True:
220
+ chunk = response.read(65536)
221
+ if not chunk:
222
+ break
223
+ total += len(chunk)
224
+ if total > max_size:
225
+ raise ValueError(f"Response content exceeded the configured maximum buffer size of {max_size} bytes.")
226
+ chunks.append(chunk)
227
+ return b"".join(chunks)
228
+
229
+ class _StreamDecompressor:
230
+ """Incremental counterpart to _decompress_content, for streamed response bodies."""
231
+
232
+ def __init__(self, content_encoding: Optional[str]):
233
+ encoding = (content_encoding or "").lower()
234
+ if encoding == "gzip":
235
+ self._decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16)
236
+ elif encoding == "deflate":
237
+ self._decompressor = zlib.decompressobj()
238
+ else:
239
+ self._decompressor = None
240
+
241
+ def decompress(self, chunk: bytes) -> bytes:
242
+ return self._decompressor.decompress(chunk) if self._decompressor else chunk
243
+
244
+ def flush(self) -> bytes:
245
+ return self._decompressor.flush() if self._decompressor else b""
246
+
247
+ class _ConnectionPool:
248
+ """Thread-safe pool of idle keep-alive connections, keyed by destination.
249
+
250
+ Also tracks connections currently in flight so HttpClientHandler.cancel_pending_requests()
251
+ can forcibly abort them.
252
+ """
253
+
254
+ def __init__(self):
255
+ self._lock = threading.Lock()
256
+ self._idle: dict[tuple, list] = {} # key -> list of (connection, idle_since)
257
+ self._in_flight: set = set()
258
+
259
+ def acquire(self, key: tuple, idle_timeout: Optional[float] = None, lifetime: Optional[float] = None):
260
+ while True:
261
+ with self._lock:
262
+ idle = self._idle.get(key)
263
+ if not idle:
264
+ return None
265
+ connection, idle_since = idle.pop()
266
+
267
+ now = time.monotonic()
268
+ created_at = getattr(connection, "_created_at", now)
269
+ expired = (idle_timeout is not None and now - idle_since > idle_timeout) or (
270
+ lifetime is not None and now - created_at > lifetime
271
+ )
272
+ if expired:
273
+ try:
274
+ connection.close()
275
+ except Exception:
276
+ pass
277
+ continue
278
+
279
+ with self._lock:
280
+ self._in_flight.add(connection)
281
+ return connection
282
+
283
+ def track(self, connection):
284
+ with self._lock:
285
+ self._in_flight.add(connection)
286
+
287
+ def untrack(self, connection):
288
+ with self._lock:
289
+ self._in_flight.discard(connection)
290
+
291
+ def discard(self, connection):
292
+ self.untrack(connection)
293
+ try:
294
+ connection.close()
295
+ except Exception:
296
+ pass
297
+
298
+ def release(self, key: tuple, connection):
299
+ self.untrack(connection)
300
+ with self._lock:
301
+ self._idle.setdefault(key, []).append((connection, time.monotonic()))
302
+
303
+ def close_all(self):
304
+ with self._lock:
305
+ all_connections = [c for entries in self._idle.values() for c, _ in entries] + list(self._in_flight)
306
+ self._idle.clear()
307
+ self._in_flight.clear()
308
+ for connection in all_connections:
309
+ try:
310
+ connection.close()
311
+ except Exception:
312
+ pass
313
+
314
+ def cancel_in_flight(self):
315
+ with self._lock:
316
+ in_flight = list(self._in_flight)
317
+ for connection in in_flight:
318
+ sock = getattr(connection, "sock", None)
319
+ if sock is not None:
320
+ try:
321
+ sock.shutdown(socket.SHUT_RDWR)
322
+ except OSError:
323
+ pass
324
+ try:
325
+ connection.close()
326
+ except Exception:
327
+ pass
328
+
329
+ class HttpMessageHandler:
330
+ async def send_async(
331
+ self,
332
+ request: HttpRequestMessage,
333
+ timeout: Optional[float] = None,
334
+ completion_option: str = HttpCompletionOption.RESPONSE_CONTENT_READ,
335
+ ) -> HttpResponseMessage:
336
+ raise NotImplementedError("Derived classes must implement send_async")
337
+
338
+ def close(self):
339
+ pass
340
+
341
+ def cancel_pending_requests(self):
342
+ pass
343
+
344
+ class DelegatingHandler(HttpMessageHandler):
345
+ """Wraps another HttpMessageHandler to add cross-cutting behavior (logging, retries,
346
+ metrics, ...) around it, mirroring System.Net.Http.DelegatingHandler. Subclass and
347
+ override send_async, calling self.inner_handler.send_async(...) to continue the chain.
348
+ """
349
+
350
+ def __init__(self, inner_handler: HttpMessageHandler):
351
+ self.inner_handler = inner_handler
352
+
353
+ async def send_async(
354
+ self,
355
+ request: HttpRequestMessage,
356
+ timeout: Optional[float] = None,
357
+ completion_option: str = HttpCompletionOption.RESPONSE_CONTENT_READ,
358
+ ) -> HttpResponseMessage:
359
+ return await self.inner_handler.send_async(request, timeout=timeout, completion_option=completion_option)
360
+
361
+ def close(self):
362
+ self.inner_handler.close()
363
+
364
+ def cancel_pending_requests(self):
365
+ self.inner_handler.cancel_pending_requests()
366
+
367
+ class _CookieJarRequestAdapter:
368
+ """Minimal adapter so http.cookiejar.CookieJar can inspect/mutate an HttpRequestMessage.
369
+
370
+ Implements the duck-typed "request" interface CookieJar.add_cookie_header/extract_cookies
371
+ expect (the same interface urllib.request.Request satisfies natively).
372
+ """
373
+
374
+ def __init__(self, request: HttpRequestMessage):
375
+ self._request = request
376
+ target = urlparse(request.request_uri)
377
+ self.host = target.hostname
378
+ self.type = target.scheme
379
+ self.origin_req_host = target.hostname
380
+ self.unverifiable = False
381
+
382
+ def get_full_url(self):
383
+ return self._request.request_uri
384
+
385
+ def has_header(self, name: str) -> bool:
386
+ return name in self._request.headers
387
+
388
+ def get_header(self, name: str, default=None):
389
+ return self._request.headers.get(name, default)
390
+
391
+ def header_items(self):
392
+ return self._request.headers.items()
393
+
394
+ def add_unredirected_header(self, name: str, value: str):
395
+ self._request.headers[name] = value
396
+
397
+ class _CookieJarResponseAdapter:
398
+ """Minimal adapter so http.cookiejar.CookieJar can read Set-Cookie headers from an HttpHeaders collection."""
399
+
400
+ def __init__(self, headers: HttpHeaders):
401
+ self._headers = headers
402
+
403
+ def info(self):
404
+ return self
405
+
406
+ def get_all(self, name: str, default=None):
407
+ values = self._headers.get_all(name)
408
+ return values if values else (default if default is not None else [])
409
+
410
+ _REDIRECT_STATUS_CODES = {
411
+ HttpStatusCode.MOVED_PERMANENTLY,
412
+ HttpStatusCode.FOUND,
413
+ HttpStatusCode.SEE_OTHER,
414
+ HttpStatusCode.TEMPORARY_REDIRECT,
415
+ HttpStatusCode.PERMANENT_REDIRECT,
416
+ }
417
+ _METHOD_DOWNGRADE_STATUS_CODES = {HttpStatusCode.MOVED_PERMANENTLY, HttpStatusCode.FOUND, HttpStatusCode.SEE_OTHER}
418
+ _BODY_PRESERVING_REDIRECT_STATUS_CODES = {HttpStatusCode.TEMPORARY_REDIRECT, HttpStatusCode.PERMANENT_REDIRECT}
419
+ # Cookie is always recomputed fresh per hop by the cookie jar rather than carried forward.
420
+ _ALWAYS_STRIP_REDIRECT_HEADERS = {"cookie"}
421
+ _CROSS_HOST_STRIP_REDIRECT_HEADERS = {"authorization", "proxy-authorization"}
422
+
423
+ class HttpClientHandler(HttpMessageHandler):
424
+ def __init__(self):
425
+ # Each entry is (certfile, keyfile) or (certfile, keyfile, password).
426
+ self.client_certificates: list[tuple[str, ...]] = []
427
+ # (username, password); sent preemptively as HTTP Basic auth.
428
+ self.credentials: Optional[tuple[str, str]] = None
429
+ self.use_proxy = False
430
+ self.proxy: Optional[str] = None
431
+ self.allow_auto_redirect = True
432
+ self.max_automatic_redirects = 50
433
+ self.use_cookies = True
434
+ self.cookies = http.cookiejar.CookieJar()
435
+ # Called with (der_encoded_cert, ssl.SSLSocket) -> bool. When set, built-in certificate
436
+ # verification (including hostname checking) is skipped entirely and this callback is
437
+ # the sole authority on whether to keep the connection - e.g. certificate pinning, or
438
+ # accepting a self-signed cert in a dev/test environment. Mirrors the responsibility
439
+ # shift of .NET's ServerCertificateCustomValidationCallback.
440
+ self.server_certificate_custom_validation: Optional[Callable[[bytes, ssl.SSLSocket], bool]] = None
441
+ # None means unbounded; matches the practical effect of .NET's ~2GB default.
442
+ self.max_response_content_buffer_size: Optional[int] = None
443
+ self.pooled_connection_idle_timeout: Optional[float] = 90.0
444
+ self.pooled_connection_lifetime: Optional[float] = None
445
+ self.max_connections_per_server: Optional[int] = None
446
+ self._pool = _ConnectionPool()
447
+ self._semaphores: dict[tuple, asyncio.Semaphore] = {}
448
+
449
+ def _build_ssl_context(self) -> ssl.SSLContext:
450
+ context = ssl.create_default_context()
451
+ if self.server_certificate_custom_validation is not None:
452
+ context.check_hostname = False
453
+ context.verify_mode = ssl.CERT_NONE
454
+ for certificate in self.client_certificates:
455
+ certfile, keyfile = certificate[0], certificate[1]
456
+ password = certificate[2] if len(certificate) > 2 else None
457
+ context.load_cert_chain(certfile, keyfile, password)
458
+ return context
459
+
460
+ def _apply_credentials(self, request: HttpRequestMessage):
461
+ if self.credentials and "Authorization" not in request.headers:
462
+ username, password = self.credentials
463
+ token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")
464
+ request.headers["Authorization"] = f"Basic {token}"
465
+
466
+ def _resolve_proxy(self) -> Optional[tuple[str, int]]:
467
+ if not (self.use_proxy and self.proxy):
468
+ return None
469
+ proxy_url = self.proxy if "://" in self.proxy else f"http://{self.proxy}"
470
+ proxy = urlparse(proxy_url)
471
+ return proxy.hostname, proxy.port or (443 if proxy.scheme == "https" else 80)
472
+
473
+ def _connection_key_and_path(self, target, use_ssl: bool, path: str) -> tuple[tuple, str]:
474
+ proxy = self._resolve_proxy()
475
+ if proxy:
476
+ proxy_host, proxy_port = proxy
477
+ if use_ssl:
478
+ return ("proxy-tunnel", proxy_host, proxy_port, target.hostname, target.port or 443), path
479
+ return ("proxy-plain", proxy_host, proxy_port), target.geturl()
480
+ return ("direct", target.scheme, target.hostname, target.port or (443 if use_ssl else 80)), path
481
+
482
+ def _get_semaphore(self, pool_key: tuple) -> Optional[asyncio.Semaphore]:
483
+ if self.max_connections_per_server is None:
484
+ return None
485
+ semaphore = self._semaphores.get(pool_key)
486
+ if semaphore is None:
487
+ semaphore = asyncio.Semaphore(self.max_connections_per_server)
488
+ self._semaphores[pool_key] = semaphore
489
+ return semaphore
490
+
491
+ def _create_connection(self, target, use_ssl: bool, timeout: Optional[float]):
492
+ proxy = self._resolve_proxy()
493
+ if proxy:
494
+ proxy_host, proxy_port = proxy
495
+ if use_ssl:
496
+ conn = http.client.HTTPSConnection(proxy_host, proxy_port, timeout=timeout, context=self._build_ssl_context())
497
+ conn.set_tunnel(target.hostname, target.port or 443)
498
+ else:
499
+ conn = http.client.HTTPConnection(proxy_host, proxy_port, timeout=timeout)
500
+ elif use_ssl:
501
+ conn = http.client.HTTPSConnection(target.hostname, target.port or 443, timeout=timeout, context=self._build_ssl_context())
502
+ else:
503
+ conn = http.client.HTTPConnection(target.hostname, target.port or 80, timeout=timeout)
504
+
505
+ if use_ssl and self.server_certificate_custom_validation is not None:
506
+ conn.connect()
507
+ der_cert = conn.sock.getpeercert(binary_form=True)
508
+ if not self.server_certificate_custom_validation(der_cert, conn.sock):
509
+ conn.close()
510
+ raise ssl.SSLCertVerificationError("Server certificate was rejected by server_certificate_custom_validation.")
511
+
512
+ conn._created_at = time.monotonic()
513
+ return conn
514
+
515
+ def _build_streaming_response(self, conn, response: http.client.HTTPResponse, pool_key: tuple) -> HttpResponseMessage:
516
+ message = HttpResponseMessage(response.status)
517
+ message.headers.update(response.headers)
518
+ message.reason_phrase = response.reason
519
+ message.version = f"{response.version // 10}.{response.version % 10}"
520
+
521
+ content_encoding = response.getheader("Content-Encoding")
522
+ if content_encoding:
523
+ message.headers.remove("Content-Encoding")
524
+ message.headers.remove("Content-Length")
525
+
526
+ def on_complete(aborted: bool):
527
+ if aborted or response.will_close:
528
+ self._pool.discard(conn)
529
+ else:
530
+ self._pool.release(pool_key, conn)
531
+
532
+ message.content = _ResponseStreamContent(response, content_encoding, on_complete)
533
+ return message
534
+
535
+ def _send_sync(
536
+ self,
537
+ target,
538
+ use_ssl: bool,
539
+ method: str,
540
+ path: str,
541
+ headers: HttpHeaders,
542
+ body,
543
+ chunked: bool,
544
+ version: str,
545
+ timeout: Optional[float],
546
+ completion_option: str,
547
+ is_streamed_upload: bool,
548
+ ) -> HttpResponseMessage:
549
+ pool_key, request_path = self._connection_key_and_path(target, use_ssl, path)
550
+
551
+ # A streamed (non-rewindable) upload body can't be safely retried on a stale
552
+ # pooled connection, so it always gets a fresh one.
553
+ conn = None if is_streamed_upload else self._pool.acquire(
554
+ pool_key, idle_timeout=self.pooled_connection_idle_timeout, lifetime=self.pooled_connection_lifetime
555
+ )
556
+ reused = conn is not None
557
+ if reused and getattr(conn, "sock", None) is not None:
558
+ conn.sock.settimeout(timeout)
559
+ if conn is None:
560
+ conn = self._create_connection(target, use_ssl, timeout)
561
+ self._pool.track(conn)
562
+
563
+ http_vsn, http_vsn_str = _SUPPORTED_HTTP_VERSIONS[version]
564
+
565
+ def do_request(connection):
566
+ connection._http_vsn, connection._http_vsn_str = http_vsn, http_vsn_str
567
+ connection.request(method, request_path, body=body, headers=headers, encode_chunked=chunked)
568
+ return connection.getresponse()
569
+
570
+ try:
571
+ try:
572
+ response = do_request(conn)
573
+ except (http.client.RemoteDisconnected, ConnectionError, OSError):
574
+ if not reused:
575
+ raise
576
+ self._pool.discard(conn)
577
+ conn = self._create_connection(target, use_ssl, timeout)
578
+ self._pool.track(conn)
579
+ response = do_request(conn)
580
+
581
+ if completion_option == HttpCompletionOption.RESPONSE_HEADERS_READ:
582
+ return self._build_streaming_response(conn, response, pool_key)
583
+
584
+ message = HttpResponseMessage.from_http_response(response, max_size=self.max_response_content_buffer_size)
585
+ if response.will_close:
586
+ self._pool.discard(conn)
587
+ else:
588
+ self._pool.release(pool_key, conn)
589
+ return message
590
+ except Exception:
591
+ self._pool.discard(conn)
592
+ raise
593
+
594
+ async def _send_once_async(
595
+ self,
596
+ request: HttpRequestMessage,
597
+ timeout: Optional[float],
598
+ completion_option: str,
599
+ ) -> HttpResponseMessage:
600
+ version = request.version or HttpVersion.HTTP_1_1
601
+ if version not in _SUPPORTED_HTTP_VERSIONS:
602
+ raise NotImplementedError(
603
+ f"HTTP/{version} is not supported. Python's standard library (http.client) only "
604
+ "implements HTTP/1.0 and HTTP/1.1. HTTP/2 requires binary framing, HPACK header compression, "
605
+ "and multiplexed stream state (see the third-party 'h2' package); HTTP/3 additionally requires "
606
+ "QUIC over UDP (see 'aioquic'). Neither is implementable with the standard library alone."
607
+ )
608
+
609
+ target = urlparse(request.request_uri)
610
+ use_ssl = target.scheme == "https"
611
+
612
+ self._apply_credentials(request)
613
+ if self.use_cookies:
614
+ self.cookies.add_cookie_header(_CookieJarRequestAdapter(request))
615
+ if "Accept-Encoding" not in request.headers:
616
+ request.headers["Accept-Encoding"] = "gzip, deflate"
617
+
618
+ is_streamed_upload = isinstance(request.content, StreamContent)
619
+ if is_streamed_upload and request.content.is_chunked and version != HttpVersion.HTTP_1_1:
620
+ raise ValueError("Chunked transfer encoding (StreamContent without a known length) requires HTTP/1.1.")
621
+
622
+ outgoing_headers = HttpHeaders()
623
+ outgoing_headers.update(request.headers)
624
+ if request.content is not None:
625
+ outgoing_headers.update(request.content.headers)
626
+
627
+ body = await request.content._open_wire_body() if request.content else None
628
+ chunked = bool(request.content and request.content.is_chunked)
629
+ path = (target.path or "/") + ("?" + target.query if target.query else "")
630
+
631
+ def send_request():
632
+ return self._send_sync(target, use_ssl, request.method, path, outgoing_headers, body, chunked, version, timeout, completion_option, is_streamed_upload)
633
+
634
+ pool_key, _ = self._connection_key_and_path(target, use_ssl, path)
635
+ semaphore = self._get_semaphore(pool_key)
636
+ if semaphore is not None:
637
+ await semaphore.acquire()
638
+ try:
639
+ message = await asyncio.get_event_loop().run_in_executor(None, send_request)
640
+ finally:
641
+ if semaphore is not None:
642
+ semaphore.release()
643
+
644
+ message.request_message = request
645
+ if self.use_cookies:
646
+ self.cookies.extract_cookies(_CookieJarResponseAdapter(message.headers), _CookieJarRequestAdapter(request))
647
+ return message
648
+
649
+ async def send_async(
650
+ self,
651
+ request: HttpRequestMessage,
652
+ timeout: Optional[float] = None,
653
+ completion_option: str = HttpCompletionOption.RESPONSE_CONTENT_READ,
654
+ ) -> HttpResponseMessage:
655
+ current_request = request
656
+ response = None
657
+
658
+ for _ in range(self.max_automatic_redirects + 1):
659
+ response = await self._send_once_async(current_request, timeout, completion_option)
660
+
661
+ if not self.allow_auto_redirect or response.status_code not in _REDIRECT_STATUS_CODES:
662
+ return response
663
+
664
+ location = response.headers.get("Location")
665
+ if not location:
666
+ return response
667
+
668
+ if response.content is not None:
669
+ await response.content.dispose_async()
670
+
671
+ next_uri = urljoin(current_request.request_uri, location)
672
+ current_scheme = urlparse(current_request.request_uri).scheme
673
+ if current_scheme == "https" and urlparse(next_uri).scheme == "http":
674
+ return response # refuse to silently downgrade from HTTPS to plaintext
675
+
676
+ next_method = current_request.method
677
+ next_content = current_request.content
678
+ if response.status_code in _METHOD_DOWNGRADE_STATUS_CODES and current_request.method not in (HttpMethod.GET, HttpMethod.HEAD):
679
+ next_method = HttpMethod.GET
680
+ next_content = None
681
+ elif response.status_code in _BODY_PRESERVING_REDIRECT_STATUS_CODES and isinstance(next_content, StreamContent):
682
+ raise RuntimeError(
683
+ f"Cannot automatically replay a StreamContent request body across a {response.status_code} "
684
+ "redirect; the stream has already been consumed and cannot be rewound. Disable "
685
+ "allow_auto_redirect or avoid streamed request bodies against redirect-prone endpoints."
686
+ )
687
+
688
+ next_request = HttpRequestMessage(next_method, next_uri, next_content, current_request.version)
689
+ same_host = urlparse(next_uri).hostname == urlparse(current_request.request_uri).hostname
690
+ for name in current_request.headers:
691
+ lower = name.lower()
692
+ if lower in _ALWAYS_STRIP_REDIRECT_HEADERS:
693
+ continue
694
+ if not same_host and lower in _CROSS_HOST_STRIP_REDIRECT_HEADERS:
695
+ continue
696
+ for value in current_request.headers.get_all(name):
697
+ next_request.headers.add(name, value)
698
+
699
+ current_request = next_request
700
+
701
+ return response
702
+
703
+ def close(self):
704
+ self._pool.close_all()
705
+
706
+ def cancel_pending_requests(self):
707
+ self._pool.cancel_in_flight()
708
+
709
+ class HttpClient:
710
+ def __init__(self, handler: HttpMessageHandler = None):
711
+ self.base_address: Optional[str] = None
712
+ self.default_request_headers = HttpHeaders()
713
+ # None means each new HttpRequestMessage falls back to HttpVersion.HTTP_1_1.
714
+ self.default_request_version: Optional[str] = None
715
+ self.timeout: Optional[float] = 100.0
716
+ self.handler = handler or HttpClientHandler()
717
+
718
+ async def send_async(
719
+ self,
720
+ request: HttpRequestMessage,
721
+ completion_option: str = HttpCompletionOption.RESPONSE_CONTENT_READ,
722
+ ) -> HttpResponseMessage:
723
+ if self.base_address:
724
+ request.request_uri = urljoin(self.base_address, request.request_uri)
725
+
726
+ if request.version is None:
727
+ request.version = self.default_request_version
728
+
729
+ for name in self.default_request_headers:
730
+ if name not in request.headers:
731
+ for value in self.default_request_headers.get_all(name):
732
+ request.headers.add(name, value)
733
+
734
+ if self.timeout is None:
735
+ return await self.handler.send_async(request, timeout=None, completion_option=completion_option)
736
+
737
+ try:
738
+ return await asyncio.wait_for(
739
+ self.handler.send_async(request, timeout=self.timeout, completion_option=completion_option),
740
+ timeout=self.timeout,
741
+ )
742
+ except asyncio.TimeoutError as exc:
743
+ raise TimeoutError(f"The request timed out after {self.timeout} seconds.") from exc
744
+
745
+ async def get_async(self, request_uri: str) -> HttpResponseMessage:
746
+ request = HttpRequestMessage(HttpMethod.GET, request_uri)
747
+ return await self.send_async(request)
748
+
749
+ async def get_byte_array_async(self, request_uri: str) -> bytes:
750
+ response = await self.get_async(request_uri)
751
+ return await response.content.read_as_bytes_async()
752
+
753
+ async def get_string_async(self, request_uri: str) -> str:
754
+ response = await self.get_async(request_uri)
755
+ return await response.content.read_as_string_async()
756
+
757
+ async def get_stream_async(self, request_uri: str) -> AsyncIterator[bytes]:
758
+ request = HttpRequestMessage(HttpMethod.GET, request_uri)
759
+ response = await self.send_async(request, completion_option=HttpCompletionOption.RESPONSE_HEADERS_READ)
760
+ async for chunk in response.content.stream_async():
761
+ yield chunk
762
+
763
+ async def post_async(self, request_uri: str, content: Optional[HttpContent] = None) -> HttpResponseMessage:
764
+ request = HttpRequestMessage(HttpMethod.POST, request_uri, content)
765
+ return await self.send_async(request)
766
+
767
+ async def put_async(self, request_uri: str, content: Optional[HttpContent] = None) -> HttpResponseMessage:
768
+ request = HttpRequestMessage(HttpMethod.PUT, request_uri, content)
769
+ return await self.send_async(request)
770
+
771
+ async def delete_async(self, request_uri: str) -> HttpResponseMessage:
772
+ request = HttpRequestMessage(HttpMethod.DELETE, request_uri)
773
+ return await self.send_async(request)
774
+
775
+ def set_base_address(self, url: str):
776
+ self.base_address = url
777
+
778
+ def default_request_headers_add(self, name: str, value: str):
779
+ self.default_request_headers.add(name, value)
780
+
781
+ def cancel_pending_requests(self):
782
+ self.handler.cancel_pending_requests()
783
+
784
+ def close(self):
785
+ self.handler.close()
786
+
787
+ async def __aenter__(self):
788
+ return self
789
+
790
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
791
+ self.close()
792
+
793
+ class HttpContent:
794
+ def __init__(self):
795
+ self.headers = HttpHeaders()
796
+
797
+ @property
798
+ def is_chunked(self) -> bool:
799
+ return False
800
+
801
+ async def read_as_bytes_async(self) -> bytes:
802
+ raise NotImplementedError("Derived classes must implement read_as_bytes_async")
803
+
804
+ async def read_as_string_async(self) -> str:
805
+ return (await self.read_as_bytes_async()).decode("utf-8")
806
+
807
+ async def read_as_json_async(self) -> Any:
808
+ return json.loads(await self.read_as_string_async())
809
+
810
+ async def dispose_async(self):
811
+ pass
812
+
813
+ async def _open_wire_body(self):
814
+ """What to hand http.client as the request body: raw bytes by default."""
815
+ return await self.read_as_bytes_async()
816
+
817
+ class ByteArrayContent(HttpContent):
818
+ def __init__(self, data: bytes, media_type: str = "application/octet-stream"):
819
+ super().__init__()
820
+ self._data = bytes(data)
821
+ self.headers["Content-Type"] = media_type
822
+ self.headers["Content-Length"] = str(len(self._data))
823
+
824
+ async def read_as_bytes_async(self) -> bytes:
825
+ return self._data
826
+
827
+ def get_length(self) -> int:
828
+ return len(self._data)
829
+
830
+ class StringContent(ByteArrayContent):
831
+ def __init__(self, text: str, media_type: str = "text/plain", encoding: str = "utf-8"):
832
+ super().__init__(text.encode(encoding), f"{media_type}; charset={encoding}")
833
+
834
+ class JsonContent(ByteArrayContent):
835
+ def __init__(self, value: Any, encoding: str = "utf-8"):
836
+ super().__init__(json.dumps(value).encode(encoding), f"application/json; charset={encoding}")
837
+
838
+ @classmethod
839
+ def create(cls, value: Any) -> JsonContent:
840
+ return cls(value)
841
+
842
+ class FormUrlEncodedContent(ByteArrayContent):
843
+ def __init__(self, data: Union[dict, list]):
844
+ pairs = data.items() if isinstance(data, dict) else data
845
+ super().__init__(urlencode(list(pairs)).encode("ascii"), "application/x-www-form-urlencoded")
846
+
847
+ class StreamContent(HttpContent):
848
+ """Content backed by a file-like object or byte-chunk iterable, sent without fully
849
+ buffering it in memory first (mirrors System.Net.Http.StreamContent).
850
+
851
+ If length is omitted, the body is sent with Transfer-Encoding: chunked, which
852
+ requires HTTP/1.1. A StreamContent instance can only be sent once - the underlying
853
+ stream can't be rewound for retries or 307/308 redirect replay.
854
+ """
855
+
856
+ def __init__(self, stream, media_type: str = "application/octet-stream", length: Optional[int] = None):
857
+ super().__init__()
858
+ self._stream = stream
859
+ self.headers["Content-Type"] = media_type
860
+ if length is not None:
861
+ self.headers["Content-Length"] = str(length)
862
+
863
+ @property
864
+ def is_chunked(self) -> bool:
865
+ return "Content-Length" not in self.headers
866
+
867
+ async def read_as_bytes_async(self) -> bytes:
868
+ data = self._stream.read()
869
+ return data if isinstance(data, bytes) else bytes(data)
870
+
871
+ async def _open_wire_body(self):
872
+ return self._stream
873
+
874
+ class _ResponseStreamContent(HttpContent):
875
+ """HttpContent backed by a live, not-yet-fully-read HTTPResponse.
876
+ Produced when HttpCompletionOption.RESPONSE_HEADERS_READ is used. Not part of the
877
+ public API - callers only see it as an HttpContent via HttpResponseMessage.content.
878
+ """
879
+
880
+ def __init__(self, response: http.client.HTTPResponse, content_encoding: Optional[str], on_complete: Callable[[bool], None]):
881
+ super().__init__()
882
+ self._response = response
883
+ self._decompressor = _StreamDecompressor(content_encoding)
884
+ self._on_complete = on_complete
885
+ self._done = False
886
+
887
+ def _finish(self, aborted: bool):
888
+ if not self._done:
889
+ self._done = True
890
+ self._on_complete(aborted)
891
+
892
+ async def stream_async(self, chunk_size: int = 8192) -> AsyncIterator[bytes]:
893
+ loop = asyncio.get_event_loop()
894
+ aborted = True
895
+ try:
896
+ while True:
897
+ raw = await loop.run_in_executor(None, self._response.read, chunk_size)
898
+ if not raw:
899
+ break
900
+ data = self._decompressor.decompress(raw)
901
+ if data:
902
+ yield data
903
+ tail = self._decompressor.flush()
904
+ if tail:
905
+ yield tail
906
+ aborted = False
907
+ finally:
908
+ self._finish(aborted)
909
+
910
+ async def read_as_bytes_async(self) -> bytes:
911
+ chunks = [chunk async for chunk in self.stream_async()]
912
+ return b"".join(chunks)
913
+
914
+ async def dispose_async(self):
915
+ self._finish(aborted=True)
916
+
917
+ class HttpRequestMessage:
918
+ def __init__(
919
+ self,
920
+ method: str = HttpMethod.GET,
921
+ request_uri: Optional[str] = None,
922
+ content: Optional[HttpContent] = None,
923
+ version: Optional[str] = None,
924
+ ):
925
+ self.method = method
926
+ self.request_uri = request_uri
927
+ self._content: Optional[HttpContent] = None
928
+ self.headers = HttpHeaders()
929
+ # None means "resolve to HttpClient.default_request_version, else HTTP/1.1" at send time.
930
+ self.version = version
931
+ # Free-form per-request metadata bag for custom handlers/middleware to use.
932
+ self.options: dict[str, Any] = {}
933
+ self.set_content(content)
934
+
935
+ @property
936
+ def content(self) -> Optional[HttpContent]:
937
+ return self._content
938
+
939
+ def set_content(self, content: Optional[HttpContent]):
940
+ if content is not None and not isinstance(content, HttpContent):
941
+ raise TypeError("content must be an HttpContent instance (e.g. StringContent, JsonContent, ByteArrayContent) or None.")
942
+ self._content = content
943
+
944
+ @property
945
+ def request_uri(self) -> Optional[str]:
946
+ return self._request_uri
947
+
948
+ @request_uri.setter
949
+ def request_uri(self, value: str):
950
+ self._request_uri = value
951
+ if value:
952
+ parsed = urlparse(value)
953
+ if parsed.scheme and parsed.netloc:
954
+ self._request_uri = value
955
+ else:
956
+ self._request_uri = parsed.path + ("?" + parsed.query if parsed.query else "")
957
+
958
+ def headers_get(self, name: str) -> Optional[str]:
959
+ return self.headers.get(name)
960
+
961
+ def headers_add(self, name: str, value: str):
962
+ self.headers.add(name, value)
963
+
964
+ def headers_remove(self, name: str):
965
+ self.headers.remove(name)
966
+
967
+ def to_string(self) -> str:
968
+ parts = [
969
+ f"{self.method} {self.request_uri or '/'} HTTP/{self.version or HttpVersion.HTTP_1_1}",
970
+ *[f"{k}: {v}" for k, v in self.headers.items()],
971
+ "",
972
+ str(self.content) if self.content else ""
973
+ ]
974
+ return "\r\n".join(parts)
975
+
976
+ class HttpResponseMessage:
977
+ @classmethod
978
+ def from_http_response(cls, response: http.client.HTTPResponse, max_size: Optional[int] = None) -> HttpResponseMessage:
979
+ message = cls(response.status)
980
+ raw_body = _read_bounded(response, max_size)
981
+ content_encoding = response.getheader("Content-Encoding")
982
+
983
+ message.headers.update(response.headers)
984
+ if content_encoding:
985
+ raw_body = _decompress_content(raw_body, content_encoding)
986
+ message.headers.remove("Content-Encoding")
987
+ message.headers.remove("Content-Length")
988
+
989
+ message.content = ByteArrayContent(raw_body)
990
+ message.reason_phrase = response.reason
991
+ message.version = f"{response.version // 10}.{response.version % 10}"
992
+ return message
993
+
994
+ def __init__(self, status_code: int = HttpStatusCode.OK):
995
+ self.status_code = status_code
996
+ self.content: Optional[HttpContent] = None
997
+ self.headers = HttpHeaders()
998
+ self.reason_phrase: Optional[str] = None
999
+ self.version: str = HttpVersion.HTTP_1_1
1000
+ self.request_message: Optional[HttpRequestMessage] = None
1001
+
1002
+ def is_success_status_code(self) -> bool:
1003
+ return 200 <= self.status_code < 300
1004
+
1005
+ def ensure_success_status_code(self):
1006
+ if not self.is_success_status_code():
1007
+ raise HttpRequestException(self)
1008
+
1009
+ def __str__(self):
1010
+ return f"HTTP/{self.version} {self.status_code} {self.reason_phrase}"
1011
+
1012
+ class HttpRequestException(Exception):
1013
+ def __init__(self, response: HttpResponseMessage):
1014
+ self.response = response
1015
+ super().__init__(f"Response status code does not indicate success: {response.status_code} ({response.reason_phrase})")
1016
+
1017
+ class HttpResponseReceivedEventArgs:
1018
+ def __init__(self, response: HttpResponseMessage):
1019
+ self.response = response
1020
+
1021
+ class HttpServiceClient:
1022
+ def __init__(self):
1023
+ self._http_client: Optional[HttpClient] = None
1024
+ self.http_response_received: Optional[Callable[[HttpResponseMessage], None]] = None
1025
+
1026
+ @property
1027
+ def http_client(self) -> HttpClient:
1028
+ if self._http_client is None:
1029
+ self._http_client = self.create_http_client()
1030
+ return self._http_client
1031
+
1032
+ def create_http_client(self) -> HttpClient:
1033
+ return HttpClient()
1034
+
1035
+ async def pre_process_http_request_message_async(self, request: HttpRequestMessage):
1036
+ pass
1037
+
1038
+ async def post_process_http_request_message_async(self, request: HttpRequestMessage):
1039
+ pass
1040
+
1041
+ async def process_http_response_message_async(self, response: HttpResponseMessage):
1042
+ pass
1043
+
1044
+ async def handle_success_status_code_async(self, response: HttpResponseMessage):
1045
+ pass
1046
+
1047
+ async def handle_non_success_status_code_async(self, response: HttpResponseMessage):
1048
+ pass
1049
+
1050
+ async def get_async(self, request_uri: str) -> HttpResponseMessage:
1051
+ return await self.execute_async(HttpMethod.GET, request_uri)
1052
+
1053
+ async def get_byte_array_async(self, request_uri: str) -> bytes:
1054
+ response = await self.get_async(request_uri)
1055
+ return await response.content.read_as_bytes_async()
1056
+
1057
+ async def get_string_async(self, request_uri: str) -> str:
1058
+ response = await self.get_async(request_uri)
1059
+ return await response.content.read_as_string_async()
1060
+
1061
+ async def get_stream_async(self, request_uri: str) -> AsyncIterator[bytes]:
1062
+ response = await self.execute_async(HttpMethod.GET, request_uri, completion_option=HttpCompletionOption.RESPONSE_HEADERS_READ)
1063
+ async for chunk in response.content.stream_async():
1064
+ yield chunk
1065
+
1066
+ async def post_async(self, request_uri: str, content: Optional[HttpContent] = None) -> HttpResponseMessage:
1067
+ return await self.execute_async(HttpMethod.POST, request_uri, content)
1068
+
1069
+ async def put_async(self, request_uri: str, content: Optional[HttpContent] = None) -> HttpResponseMessage:
1070
+ return await self.execute_async(HttpMethod.PUT, request_uri, content)
1071
+
1072
+ async def patch_async(self, request_uri: str, content: Optional[HttpContent] = None) -> HttpResponseMessage:
1073
+ return await self.execute_async(HttpMethod.PATCH, request_uri, content)
1074
+
1075
+ async def delete_async(self, request_uri: str, content: Optional[HttpContent] = None) -> HttpResponseMessage:
1076
+ return await self.execute_async(HttpMethod.DELETE, request_uri, content)
1077
+
1078
+ async def execute_async(
1079
+ self,
1080
+ method: str,
1081
+ request_uri: str,
1082
+ content: Optional[HttpContent] = None,
1083
+ http_request_message_modifier: Optional[Callable[[HttpRequestMessage], None]] = None,
1084
+ completion_option: str = HttpCompletionOption.RESPONSE_CONTENT_READ,
1085
+ ) -> HttpResponseMessage:
1086
+ request = HttpRequestMessage(method, request_uri, content)
1087
+
1088
+ await self.pre_process_http_request_message_async(request)
1089
+ if http_request_message_modifier:
1090
+ http_request_message_modifier(request)
1091
+ await self.post_process_http_request_message_async(request)
1092
+
1093
+ response = await self.http_client.send_async(request, completion_option=completion_option)
1094
+
1095
+ if self.http_response_received:
1096
+ self.http_response_received(HttpResponseReceivedEventArgs(response))
1097
+
1098
+ await self.process_http_response_message_async(response)
1099
+
1100
+ if response.is_success_status_code():
1101
+ await self.handle_success_status_code_async(response)
1102
+ else:
1103
+ await self.handle_non_success_status_code_async(response)
1104
+
1105
+ return response
1106
+
1107
+ async def send_async(
1108
+ self,
1109
+ request: HttpRequestMessage,
1110
+ completion_option: str = HttpCompletionOption.RESPONSE_CONTENT_READ,
1111
+ ) -> HttpResponseMessage:
1112
+ await self.pre_process_http_request_message_async(request)
1113
+ await self.post_process_http_request_message_async(request)
1114
+
1115
+ response = await self.http_client.send_async(request, completion_option=completion_option)
1116
+
1117
+ if self.http_response_received:
1118
+ self.http_response_received(HttpResponseReceivedEventArgs(response))
1119
+
1120
+ await self.process_http_response_message_async(response)
1121
+
1122
+ if response.is_success_status_code():
1123
+ await self.handle_success_status_code_async(response)
1124
+ else:
1125
+ await self.handle_non_success_status_code_async(response)
1126
+
1127
+ return response
1128
+
1129
+ async def __aenter__(self):
1130
+ return self
1131
+
1132
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
1133
+ if self._http_client is not None:
1134
+ self._http_client.close()
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: universal-common-net-http
3
+ Version: 1.0.0
4
+ Summary: Library for HTTP operations.
5
+ Author-email: Andrew Ong <ong.andrew@gmail.com>
6
+ License-File: LICENSE
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.9
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest; extra == 'test'
14
+ Requires-Dist: pytest-asyncio; extra == 'test'
@@ -0,0 +1,5 @@
1
+ universal_common_net_http/__init__.py,sha256=nSntwpGzJ5-BWw6rak_iqfQw3pSo5969X1_fXmUA3Es,43701
2
+ universal_common_net_http-1.0.0.dist-info/METADATA,sha256=TEatbSzwi7aZkC2h0oncDYYggjPIG_FoAByk3VzWiXc,534
3
+ universal_common_net_http-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
4
+ universal_common_net_http-1.0.0.dist-info/licenses/LICENSE,sha256=oZ1YqqsVxNABnladHAc9G1KG_dN9vu56WKfRrnYEWuE,7167
5
+ universal_common_net_http-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,121 @@
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.