_config.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. from __future__ import annotations
  2. import os
  3. import typing
  4. from ._models import Headers
  5. from ._types import CertTypes, HeaderTypes, TimeoutTypes
  6. from ._urls import URL
  7. if typing.TYPE_CHECKING:
  8. import ssl # pragma: no cover
  9. __all__ = ["Limits", "Proxy", "Timeout", "create_ssl_context"]
  10. class UnsetType:
  11. pass # pragma: no cover
  12. UNSET = UnsetType()
  13. def create_ssl_context(
  14. verify: ssl.SSLContext | str | bool = True,
  15. cert: CertTypes | None = None,
  16. trust_env: bool = True,
  17. ) -> ssl.SSLContext:
  18. import ssl
  19. import warnings
  20. import certifi
  21. if verify is True:
  22. if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover
  23. ctx = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"])
  24. elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma: nocover
  25. ctx = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"])
  26. else:
  27. # Default case...
  28. ctx = ssl.create_default_context(cafile=certifi.where())
  29. elif verify is False:
  30. ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  31. ctx.check_hostname = False
  32. ctx.verify_mode = ssl.CERT_NONE
  33. elif isinstance(verify, str): # pragma: nocover
  34. message = (
  35. "`verify=<str>` is deprecated. "
  36. "Use `verify=ssl.create_default_context(cafile=...)` "
  37. "or `verify=ssl.create_default_context(capath=...)` instead."
  38. )
  39. warnings.warn(message, DeprecationWarning)
  40. if os.path.isdir(verify):
  41. return ssl.create_default_context(capath=verify)
  42. return ssl.create_default_context(cafile=verify)
  43. else:
  44. ctx = verify
  45. if cert: # pragma: nocover
  46. message = (
  47. "`cert=...` is deprecated. Use `verify=<ssl_context>` instead,"
  48. "with `.load_cert_chain()` to configure the certificate chain."
  49. )
  50. warnings.warn(message, DeprecationWarning)
  51. if isinstance(cert, str):
  52. ctx.load_cert_chain(cert)
  53. else:
  54. ctx.load_cert_chain(*cert)
  55. return ctx
  56. class Timeout:
  57. """
  58. Timeout configuration.
  59. **Usage**:
  60. Timeout(None) # No timeouts.
  61. Timeout(5.0) # 5s timeout on all operations.
  62. Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts.
  63. Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere.
  64. Timeout(5.0, pool=None) # No timeout on acquiring connection from pool.
  65. # 5s timeout elsewhere.
  66. """
  67. def __init__(
  68. self,
  69. timeout: TimeoutTypes | UnsetType = UNSET,
  70. *,
  71. connect: None | float | UnsetType = UNSET,
  72. read: None | float | UnsetType = UNSET,
  73. write: None | float | UnsetType = UNSET,
  74. pool: None | float | UnsetType = UNSET,
  75. ) -> None:
  76. if isinstance(timeout, Timeout):
  77. # Passed as a single explicit Timeout.
  78. assert connect is UNSET
  79. assert read is UNSET
  80. assert write is UNSET
  81. assert pool is UNSET
  82. self.connect = timeout.connect # type: typing.Optional[float]
  83. self.read = timeout.read # type: typing.Optional[float]
  84. self.write = timeout.write # type: typing.Optional[float]
  85. self.pool = timeout.pool # type: typing.Optional[float]
  86. elif isinstance(timeout, tuple):
  87. # Passed as a tuple.
  88. self.connect = timeout[0]
  89. self.read = timeout[1]
  90. self.write = None if len(timeout) < 3 else timeout[2]
  91. self.pool = None if len(timeout) < 4 else timeout[3]
  92. elif not (
  93. isinstance(connect, UnsetType)
  94. or isinstance(read, UnsetType)
  95. or isinstance(write, UnsetType)
  96. or isinstance(pool, UnsetType)
  97. ):
  98. self.connect = connect
  99. self.read = read
  100. self.write = write
  101. self.pool = pool
  102. else:
  103. if isinstance(timeout, UnsetType):
  104. raise ValueError(
  105. "httpx.Timeout must either include a default, or set all "
  106. "four parameters explicitly."
  107. )
  108. self.connect = timeout if isinstance(connect, UnsetType) else connect
  109. self.read = timeout if isinstance(read, UnsetType) else read
  110. self.write = timeout if isinstance(write, UnsetType) else write
  111. self.pool = timeout if isinstance(pool, UnsetType) else pool
  112. def as_dict(self) -> dict[str, float | None]:
  113. return {
  114. "connect": self.connect,
  115. "read": self.read,
  116. "write": self.write,
  117. "pool": self.pool,
  118. }
  119. def __eq__(self, other: typing.Any) -> bool:
  120. return (
  121. isinstance(other, self.__class__)
  122. and self.connect == other.connect
  123. and self.read == other.read
  124. and self.write == other.write
  125. and self.pool == other.pool
  126. )
  127. def __repr__(self) -> str:
  128. class_name = self.__class__.__name__
  129. if len({self.connect, self.read, self.write, self.pool}) == 1:
  130. return f"{class_name}(timeout={self.connect})"
  131. return (
  132. f"{class_name}(connect={self.connect}, "
  133. f"read={self.read}, write={self.write}, pool={self.pool})"
  134. )
  135. class Limits:
  136. """
  137. Configuration for limits to various client behaviors.
  138. **Parameters:**
  139. * **max_connections** - The maximum number of concurrent connections that may be
  140. established.
  141. * **max_keepalive_connections** - Allow the connection pool to maintain
  142. keep-alive connections below this point. Should be less than or equal
  143. to `max_connections`.
  144. * **keepalive_expiry** - Time limit on idle keep-alive connections in seconds.
  145. """
  146. def __init__(
  147. self,
  148. *,
  149. max_connections: int | None = None,
  150. max_keepalive_connections: int | None = None,
  151. keepalive_expiry: float | None = 5.0,
  152. ) -> None:
  153. self.max_connections = max_connections
  154. self.max_keepalive_connections = max_keepalive_connections
  155. self.keepalive_expiry = keepalive_expiry
  156. def __eq__(self, other: typing.Any) -> bool:
  157. return (
  158. isinstance(other, self.__class__)
  159. and self.max_connections == other.max_connections
  160. and self.max_keepalive_connections == other.max_keepalive_connections
  161. and self.keepalive_expiry == other.keepalive_expiry
  162. )
  163. def __repr__(self) -> str:
  164. class_name = self.__class__.__name__
  165. return (
  166. f"{class_name}(max_connections={self.max_connections}, "
  167. f"max_keepalive_connections={self.max_keepalive_connections}, "
  168. f"keepalive_expiry={self.keepalive_expiry})"
  169. )
  170. class Proxy:
  171. def __init__(
  172. self,
  173. url: URL | str,
  174. *,
  175. ssl_context: ssl.SSLContext | None = None,
  176. auth: tuple[str, str] | None = None,
  177. headers: HeaderTypes | None = None,
  178. ) -> None:
  179. url = URL(url)
  180. headers = Headers(headers)
  181. if url.scheme not in ("http", "https", "socks5", "socks5h"):
  182. raise ValueError(f"Unknown scheme for proxy URL {url!r}")
  183. if url.username or url.password:
  184. # Remove any auth credentials from the URL.
  185. auth = (url.username, url.password)
  186. url = url.copy_with(username=None, password=None)
  187. self.url = url
  188. self.auth = auth
  189. self.headers = headers
  190. self.ssl_context = ssl_context
  191. @property
  192. def raw_auth(self) -> tuple[bytes, bytes] | None:
  193. # The proxy authentication as raw bytes.
  194. return (
  195. None
  196. if self.auth is None
  197. else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8"))
  198. )
  199. def __repr__(self) -> str:
  200. # The authentication is represented with the password component masked.
  201. auth = (self.auth[0], "********") if self.auth else None
  202. # Build a nice concise representation.
  203. url_str = f"{str(self.url)!r}"
  204. auth_str = f", auth={auth!r}" if auth else ""
  205. headers_str = f", headers={dict(self.headers)!r}" if self.headers else ""
  206. return f"Proxy({url_str}{auth_str}{headers_str})"
  207. DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0)
  208. DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20)
  209. DEFAULT_MAX_REDIRECTS = 20