Source code for urlfetch

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
urlfetch
~~~~~~~~~~

An easy to use HTTP client based on httplib.

:copyright: (c) 2011-2026 by Yue Du.
:license: BSD 2-clause License, see LICENSE for more details.
"""

__version__ = "3.0.1"
__author__ = "Yue Du <ifduyue@gmail.com>"
__url__ = "https://github.com/ifduyue/urlfetch"
__license__ = "BSD 2-Clause License"

import zlib
import os, sys, base64, codecs, uuid, stat, time, socket
import ssl
import mimetypes
from os.path import basename, dirname, abspath, join as pathjoin
from functools import partial
from io import BytesIO
import re
import ipaddress

import json

json_dumps = json.dumps
json_loads = json.loads

from http.client import HTTPConnection, HTTPSConnection
from urllib.parse import parse_qs, urlencode, urlsplit, urljoin
import http.cookies as Cookie
import http.cookiejar as cookiejar

__all__ = (
    "request",
    "fetch",
    "Session",
    "get",
    "head",
    "put",
    "post",
    "delete",
    "options",
    "trace",
    "patch",
    "UrlfetchException",
    "ContentLimitExceeded",
    "URLError",
    "ContentDecodingError",
    "TooManyRedirects",
    "Timeout",
    "HTTPError",
)

GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
HEAD = "HEAD"
OPTIONS = "OPTIONS"
TRACE = "TRACE"
PATCH = "PATCH"


[docs] class UrlfetchException(IOError): "Base exception. All exceptions and errors will subclass from this."
[docs] class ContentLimitExceeded(UrlfetchException): "Content length is beyond the limit."
[docs] class URLError(UrlfetchException, ValueError): "Error parsing or handling the URL."
[docs] class ContentDecodingError(UrlfetchException): "Failed to decode the content."
[docs] class TooManyRedirects(UrlfetchException): """Too many redirects."""
[docs] class Timeout(UrlfetchException): """Request timed out."""
[docs] class HTTPError(UrlfetchException): """HTTP status code is 400 or higher.""" def __init__(self, response): self.response = response self.status = self.status_code = response.status self.reason = response.reason UrlfetchException.__init__( self, "%s %s" % (response.status, response.reason) )
class cached_property: """Cached property. A property that is only computed once per instance and then replaces itself with an ordinary attribute. Deleting the attribute resets the property. """ def __init__(self, fget, fset=None, fdel=None, doc=None): self.__get = fget self.__set = fset self.__del = fdel self.__doc__ = doc or fget.__doc__ self.__name__ = fget.__name__ self.__module__ = fget.__module__ def __get__(self, instance, owner): if instance is None: # attribute is accessed through the owner class return self try: return instance.__dict__[self.__name__] except KeyError: value = instance.__dict__[self.__name__] = self.__get(instance) return value def __set__(self, instance, value): if instance is None: return self if self.__set is not None: value = self.__set(instance, value) instance.__dict__[self.__name__] = value def __delete__(self, instance): if instance is None: return self try: value = instance.__dict__.pop(self.__name__) except KeyError: pass else: if self.__del is not None: self.__del(instance, value) def setter(self, fset): return self.__class__(self.__get, fset, self.__del) def deleter(self, fdel): return self.__class__(self.__get, self.__set, fdel) ############################################################################## # Core Methods and Classes #################################################### ##############################################################################
[docs] class Response: """A Response object. >>> import urlfetch >>> response = urlfetch.get("http://docs.python.org/") >>> response.total_time 0.033042049407959 >>> response.status, response.reason, response.version (200, 'OK', 10) >>> type(response.body), len(response.body) (<type 'str'>, 8719) >>> type(response.text), len(response.text) (<type 'unicode'>, 8719) >>> response.getheader('server') 'Apache/2.2.16 (Debian)' >>> response.getheaders() [ ('content-length', '8719'), ('x-cache', 'MISS from localhost'), ('accept-ranges', 'bytes'), ('vary', 'Accept-Encoding'), ('server', 'Apache/2.2.16 (Debian)'), ('last-modified', 'Tue, 26 Jun 2012 19:23:18 GMT'), ('connection', 'close'), ('etag', '"13cc5e4-220f-4c36507ded580"'), ('date', 'Wed, 27 Jun 2012 06:50:30 GMT'), ('content-type', 'text/html'), ('x-cache-lookup', 'MISS from localhost:8080') ] >>> response.headers { 'content-length': '8719', 'x-cache': 'MISS from localhost', 'accept-ranges': 'bytes', 'vary': 'Accept-Encoding', 'server': 'Apache/2.2.16 (Debian)', 'last-modified': 'Tue, 26 Jun 2012 19:23:18 GMT', 'connection': 'close', 'etag': '"13cc5e4-220f-4c36507ded580"', 'date': 'Wed, 27 Jun 2012 06:50:30 GMT', 'content-type': 'text/html', 'x-cache-lookup': 'MISS from localhost:8080' } :raises: :class:`ContentLimitExceeded` """ def __init__(self, r, **kwargs): self._conn = kwargs.pop("connection", None) for k in kwargs: setattr(self, k, kwargs[k]) self._r = r # httplib.HTTPResponse self.msg = r.msg #: Status code returned by server. self.status = r.status #: An alias of :attr:`status`. self.status_code = r.status #: Reason phrase returned by server. self.reason = r.reason #: HTTP protocol version used by server. #: 10 for HTTP/1.0, 11 for HTTP/1.1. self.version = r.version #: total time self.total_time = kwargs.pop("total_time", None) self.getheader = r.getheader self.getheaders = r.getheaders self._content_encoding = self.getheader("content-encoding", None) self._decoder = None try: self.length_limit = int(kwargs.get("length_limit") or 0) or None except (TypeError, ValueError): self.length_limit = None # if content (length) size is more than length_limit, skip try: content_length = int(self.getheader("Content-Length", 0) or 0) except (TypeError, ValueError): content_length = 0 if self.length_limit and content_length > self.length_limit: self.close() raise ContentLimitExceeded( "Content length is more than %d bytes" % self.length_limit ) @property def ok(self): """``True`` if :attr:`status` is less than 400.""" return self.status < 400
[docs] def raise_for_status(self): """Raise :class:`HTTPError` if :attr:`status` is 400 or higher.""" if self.status >= 400: raise HTTPError(self)
[docs] def read(self, chunk_size=65536): """Read content (for streaming and large files) :arg int chunk_size: size of chunk, default is 65536, i.e. 64KiB. """ return self._r.read(chunk_size)
def __iter__(self): return self def __next__(self): chunk = self.read() if not chunk: if self._decoder: chunk = self._decoder.flush() self._decoder = None return chunk else: raise StopIteration else: ce = self._content_encoding if ce in ("gzip", "deflate"): if not self._decoder: if ce == "gzip": self._decoder = zlib.decompressobj(16 + zlib.MAX_WBITS) else: self._decoder = zlib.decompressobj() try: return self._decoder.decompress(chunk) except zlib.error: self._decoder = zlib.decompressobj(-zlib.MAX_WBITS) try: return self._decoder.decompress(chunk) except (IOError, zlib.error) as e: self.close() raise ContentDecodingError(e) if ce: self.close() raise ContentDecodingError("Unknown encoding: %s" % ce) return chunk
[docs] def iter_lines(self, delimiter=None): """Yield decoded chunks split on ``delimiter`` (default ``\\n``).""" pending = b"" sep = delimiter if delimiter is not None else b"\n" for chunk in self: pending += chunk lines = pending.split(sep) pending = lines.pop() for line in lines: yield line if pending: yield pending
def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() return False
[docs] @classmethod def from_httplib(cls, r, **kwargs): """Make an :class:`~urlfetch.Response` object from a httplib response object.""" return cls(r, **kwargs)
[docs] @cached_property def body(self): """Response body. :raises: :class:`ContentLimitExceeded`, :class:`ContentDecodingError` """ content = [] length = 0 for chunk in self: content.append(chunk) length += len(chunk) if self.length_limit and length > self.length_limit: self.close() raise ContentLimitExceeded( "Content length is more than %d " "bytes" % self.length_limit ) return b"".join(content)
# compatible with requests #: An alias of :attr:`body`. @property def content(self): return self.body
[docs] @cached_property def encoding(self): """Charset from ``Content-Type``, or ``None``.""" header = self.getheader("content-type") or "" match = re.search(r"charset=([^\s;]+)", header, re.I) if match: return match.group(1).strip("\"'") return None
[docs] @cached_property def text(self): """Response body in str. Decoded with :attr:`encoding` if ``Content-Type`` has a charset, otherwise UTF-8. Raises :class:`ContentDecodingError` on failure. """ enc = self.encoding or "utf-8" try: return self.content.decode(enc) except (LookupError, UnicodeDecodeError) as e: raise ContentDecodingError(e)
[docs] @cached_property def json(self): """Load response body as json. :raises: :class:`ContentDecodingError` """ try: return json_loads(self.text) except Exception as e: raise ContentDecodingError(e)
[docs] @cached_property def headers(self): """Response headers. Response headers is a dict with all keys in lower case. >>> import urlfetch >>> response = urlfetch.get("http://docs.python.org/") >>> response.headers { 'content-length': '8719', 'x-cache': 'MISS from localhost', 'accept-ranges': 'bytes', 'vary': 'Accept-Encoding', 'server': 'Apache/2.2.16 (Debian)', 'last-modified': 'Tue, 26 Jun 2012 19:23:18 GMT', 'connection': 'close', 'etag': '"13cc5e4-220f-4c36507ded580"', 'date': 'Wed, 27 Jun 2012 06:50:30 GMT', 'content-type': 'text/html', 'x-cache-lookup': 'MISS from localhost:8080' } """ return dict((k.lower(), v) for k, v in self.getheaders())
[docs] @cached_property def cookies(self): """Cookies in dict""" values = [] if hasattr(self.msg, "get_all"): values = self.msg.get_all("set-cookie") or [] else: header = self.getheader("set-cookie") if header: values = [header] cookies = {} for header in values: c = Cookie.SimpleCookie() try: c.load(header) except Cookie.CookieError: continue cookies.update((i.key, i.value) for i in c.values()) return cookies
[docs] @cached_property def cookiestring(self): """Cookie string""" return "; ".join("%s=%s" % (k, v) for k, v in self.cookies.items())
[docs] def close(self): """Close the connection.""" try: if self._r is not None: self._r.close() finally: conn = getattr(self, "_conn", None) self._conn = None if conn is not None: try: conn.close() except Exception: pass
def __del__(self): try: self.close() except Exception: pass
class _CookieRequest: """Minimal request object for :mod:`http.cookiejar`.""" def __init__(self, url): parsed = parse_url(url) path = parsed["uri"] or "/" self.full_url = "%s://%s%s" % (parsed["scheme"], parsed["http_host"], path) self.type = parsed["scheme"] self.host = parsed["http_host"] self.origin_req_host = parsed["host"] self.unverifiable = False self.headers = {} self.unredirected_hdrs = {} def get_full_url(self): return self.full_url def get_origin_req_host(self): return self.origin_req_host def get_header(self, name, default=None): return self.headers.get(name, default) def has_header(self, name): lname = name.lower() return any(k.lower() == lname for k in self.headers) or any( k.lower() == lname for k in self.unredirected_hdrs ) def add_unredirected_header(self, key, val): self.unredirected_hdrs[key] = val class _CookieResponse: def __init__(self, msg): self._msg = msg def info(self): return self._msg def _make_cookie(name, value, domain, path="/"): return cookiejar.Cookie( 0, name, str(value), None, False, domain, True, domain.startswith("."), path or "/", True, False, None, True, None, None, {}, )
[docs] class Session: """A session object. :class:`urlfetch.Session` holds common headers and a cookiejar. Cookies from responses, :meth:`putcookie`, and the ``cookies`` argument are only sent back to matching hosts. A cookie without an explicit ``domain`` is bound to the host of the next request. :arg dict headers: Init headers. :arg dict cookies: Init cookies (bound to the first request host). :arg tuple auth: (username, password) for basic authentication. """ def __init__(self, headers=None, cookies=None, auth=None): """Init a :class:`~urlfetch.Session` object""" #: headers self.headers = {} if headers is None else headers.copy() self._jar = cookiejar.CookieJar() self._pending = {} if cookies is None else dict(cookies) if auth and isinstance(auth, (list, tuple)): auth = "%s:%s" % tuple(auth) auth = base64.b64encode(auth.encode("utf-8")) self.headers["Authorization"] = "Basic " + auth.decode("utf-8")
[docs] def putheader(self, header, value): """Add an header to default headers.""" self.headers[header] = value
[docs] def popheader(self, header): """Remove an header from default headers.""" return self.headers.pop(header)
[docs] def putcookie(self, key, value="", domain=None, path="/"): """Add a cookie. If *domain* is omitted, the cookie is bound to the host of the next request. """ if domain: self._jar.set_cookie(_make_cookie(key, value, domain, path)) self._pending.pop(key, None) else: self._pending[key] = value
[docs] def popcookie(self, key): """Remove a cookie from pending cookies and the cookiejar.""" if key in self._pending: return self._pending.pop(key) for c in list(self._jar): if c.name == key: self._jar.clear(c.domain, c.path, c.name) return c.value raise KeyError(key)
@property def cookies(self): """Cookies in dict (pending cookies plus those from the cookiejar).""" d = dict(self._pending) for c in self._jar: d[c.name] = c.value return d @cookies.setter def cookies(self, value): self._pending = {} if value is None else dict(value) self._jar = cookiejar.CookieJar() @property def cookiestring(self): """Cookie string. It's assignalbe, and will change :attr:`~.Session.cookies` correspondingly. >>> s = Session() >>> s.cookiestring = 'foo=bar; 1=2' >>> s.cookies {'1': '2', 'foo': 'bar'} """ return "; ".join("%s=%s" % (k, v) for k, v in self.cookies.items()) @cookiestring.setter def cookiestring(self, value): """"Cookie string setter""" c = Cookie.SimpleCookie(value) self.cookies = dict((i.key, i.value) for i in c.values())
[docs] def snapshot(self): session = {"headers": self.headers.copy(), "cookies": self.cookies.copy()} return session
[docs] def _bind_pending(self, url): if not self._pending or not url: return host = parse_url(url)["host"] for name, value in self._pending.items(): self._jar.set_cookie(_make_cookie(name, value, host)) self._pending = {}
[docs] def _store_cookies(self, r): url = getattr(r, "url", None) msg = getattr(r, "msg", None) if not url or msg is None: return self._jar.extract_cookies(_CookieResponse(msg), _CookieRequest(url))
[docs] def request(self, *args, **kwargs): """Issue a request.""" headers = self.headers.copy() url = kwargs.get("url", args[0] if args else None) self._bind_pending(url) cookie_header = self._cookie_header(url) if cookie_header: headers["Cookie"] = cookie_header headers.update(kwargs.get("headers") or {}) kwargs["headers"] = headers r = request(*args, **kwargs) self._store_cookies(r) return r
[docs] def fetch(self, *args, **kwargs): """Fetch an URL""" data = kwargs.get("data", None) files = kwargs.get("files") or {} if ( kwargs.get("json") is not None or (data and isinstance(data, (str, bytes, dict))) or files ): return self.post(*args, **kwargs) return self.get(*args, **kwargs)
[docs] def get(self, *args, **kwargs): """Issue a get request.""" kwargs["method"] = "GET" return self.request(*args, **kwargs)
[docs] def post(self, *args, **kwargs): """Issue a post request.""" kwargs["method"] = POST return self.request(*args, **kwargs)
[docs] def put(self, *args, **kwargs): """Issue a put request.""" kwargs["method"] = PUT return self.request(*args, **kwargs)
[docs] def delete(self, *args, **kwargs): """Issue a delete request.""" kwargs["method"] = DELETE return self.request(*args, **kwargs)
[docs] def head(self, *args, **kwargs): """Issue a head request.""" kwargs["method"] = HEAD return self.request(*args, **kwargs)
[docs] def options(self, *args, **kwargs): """Issue a options request.""" kwargs["method"] = OPTIONS return self.request(*args, **kwargs)
[docs] def trace(self, *args, **kwargs): """Issue a trace request.""" kwargs["method"] = TRACE return self.request(*args, **kwargs)
[docs] def patch(self, *args, **kwargs): """Issue a patch request.""" kwargs["method"] = PATCH return self.request(*args, **kwargs)
[docs] def fetch(*args, **kwargs): """fetch an URL. :func:`~urlfetch.fetch` is a wrapper of :func:`~urlfetch.request`. It calls :func:`~urlfetch.get` by default. If one of parameter ``data`` or parameter ``files`` is supplied, :func:`~urlfetch.post` is called. """ data = kwargs.get("data", None) files = kwargs.get("files") or {} if ( kwargs.get("json") is not None or (data and isinstance(data, (str, bytes, dict))) or files ): return post(*args, **kwargs) return get(*args, **kwargs)
REDIRECT_STATUSES = (301, 302, 303, 307, 308) PRESERVE_METHOD_REDIRECTS = (307, 308) def _origin(parsed): port = parsed["port"] if port is None: port = 443 if parsed["scheme"] == "https" else 80 return parsed["scheme"], parsed["host"], port def _no_proxy_hosts(trust_env): ignore_hosts = list(PROXY_IGNORE_HOSTS) if trust_env: no_proxy = os.getenv("no_proxy") or os.getenv("NO_PROXY") if no_proxy: ignore_hosts = [ h.strip() for h in no_proxy.replace(" ", ",").split(",") if h.strip() ] return ignore_hosts def _close_conn(conn): if conn is None: return try: conn.close() except Exception: pass def _strip_host_port(value): value = value.strip() if value.startswith("["): end = value.find("]") if end != -1: return value[1:end] return value.strip("[]") if value.count(":") == 1: name, port = value.rsplit(":", 1) if port.isdigit(): return name return value def match_no_proxy(host, no_proxy): """Return True if *host* should bypass the proxy for a no_proxy rule.""" if not host or not no_proxy: return False host = _strip_host_port(host) if "%" in host and ":" in host: host = host.split("%", 1)[0] rule = no_proxy.strip() if rule == "*": return True rule = _strip_host_port(rule) try: host_ip = ipaddress.ip_address(host) except ValueError: host_ip = None if "/" in rule: try: network = ipaddress.ip_network(rule, strict=False) except ValueError: return False return host_ip is not None and host_ip in network if host_ip is not None: try: return host_ip == ipaddress.ip_address(rule) except ValueError: return False host_l = host.lower().rstrip(".") rule_l = rule.lower().lstrip(".").rstrip(".") if not rule_l: return False return host_l == rule_l or host_l.endswith("." + rule_l) def _merge_headers(base, extra): """Overlay extra onto base. Header names are case-insensitive. An existing name keeps its spelling, so later lookups of Host, Cookie, Authorization, and Content-Type still match. The new value replaces it. """ if not extra: return folded = {name.lower(): name for name in base} for name, value in extra.items(): current = folded.get(name.lower()) if current is None: base[name] = value folded[name.lower()] = name else: base[current] = value
[docs] def request( url, method="GET", params=None, data=None, headers=None, timeout=None, files=None, randua=False, auth=None, length_limit=None, proxies=None, trust_env=True, max_redirects=0, source_address=None, validate_certificate=None, ssl_context=None, json=None, **kwargs ): """request an URL :arg string url: URL to be fetched. :arg string method: (optional) HTTP method, one of ``GET``, ``DELETE``, ``HEAD``, ``OPTIONS``, ``PUT``, ``POST``, ``TRACE``, ``PATCH``. ``GET`` is the default. :arg dict/string params: (optional) Dict or string to attach to url as querystring. :arg dict headers: (optional) HTTP request headers. :arg float timeout: (optional) Timeout in seconds :arg files: (optional) Files to be sended :arg randua: (optional) If ``True`` or ``path string``, use a random user-agent in headers, instead of ``'urlfetch/' + __version__`` :arg tuple auth: (optional) (username, password) for basic authentication :arg int length_limit: (optional) If ``None``, no limits on content length, if the limit reached raised exception 'Content length is more than ...' :arg dict proxies: (optional) HTTP proxy, like {'http': '127.0.0.1:8888', 'https': '127.0.0.1:563'} :arg bool trust_env: (optional) If ``True``, urlfetch will get infomations from env, such as HTTP_PROXY, HTTPS_PROXY :arg int max_redirects: (integer, optional) Max redirects allowed within a request. Default is 0, which means redirects are not allowed. :arg tuple source_address: (optional) A tuple of (host, port) to specify the source_address to bind to. :arg json: (optional) JSON-serializable object sent as the request body with ``Content-Type: application/json``. Cannot be combined with ``data`` or ``files``. :arg ssl.SSLContext ssl_context: (optional) SSL context for HTTPS connections. Overrides ``validate_certificate``. :arg bool validate_certificate: (optional) If ``False``, urlfetch skips all the necessary certificate and hostname checks :returns: A :class:`~urlfetch.Response` object :raises: :class:`URLError`, :class:`UrlfetchException`, :class:`TooManyRedirects`, """ def _proxy_auth_header(parsed_proxy): if not (parsed_proxy and parsed_proxy["username"] and parsed_proxy["password"]): return None token = "%s:%s" % (parsed_proxy["username"], parsed_proxy["password"]) token = base64.b64encode(token.encode("utf-8")).decode("utf-8") return "Basic " + token def make_connection(conn_type, host, port, timeout, source_address): """Return HTTP or HTTPS connection.""" conn_kw = {"timeout": timeout, "source_address": source_address} ctx = ssl_context if ctx is None and validate_certificate is False: ctx = ssl._create_unverified_context() if conn_type == "http": return HTTPConnection(host, port, **conn_kw) if conn_type == "https": return HTTPSConnection(host, port, context=ctx, **conn_kw) raise URLError("Unknown Connection Type: %s" % conn_type) def open_connection(parsed_url): scheme = parsed_url["scheme"] proxy = proxies.get(scheme) if proxy and not any( match_no_proxy(parsed_url["host"], host) for host in ignore_hosts ): if "://" not in proxy: proxy = "http://%s" % proxy parsed_proxy = parse_url(proxy) if scheme == "https": conn = make_connection( "https", parsed_proxy["host"], parsed_proxy["port"], timeout, source_address, ) tunnel_headers = {} auth = _proxy_auth_header(parsed_proxy) if auth: tunnel_headers["Proxy-Authorization"] = auth conn.set_tunnel( parsed_url["host"], parsed_url["port"] or 443, tunnel_headers ) return conn, True, True, None conn = make_connection( parsed_proxy["scheme"], parsed_proxy["host"], parsed_proxy["port"], timeout, source_address, ) auth = _proxy_auth_header(parsed_proxy) return conn, True, False, auth conn = make_connection( scheme, parsed_url["host"], parsed_url["port"], timeout, source_address ) return conn, False, False, None def perform(url, method, data, reqheaders, parsed_url): conn = None try: conn, via_proxy, tunneled, proxy_auth = open_connection(parsed_url) if via_proxy and not tunneled and proxy_auth: reqheaders["Proxy-Authorization"] = proxy_auth else: reqheaders.pop("Proxy-Authorization", None) request_url = url if (via_proxy and not tunneled) else parsed_url["uri"] conn.request(method, request_url, data, reqheaders) resp = conn.getresponse() return conn, resp except socket.timeout as e: _close_conn(conn) raise Timeout(e) except Exception as e: _close_conn(conn) if isinstance(e, (Timeout, UrlfetchException)): raise raise UrlfetchException(e) headers = {} if headers is None else headers files = {} if files is None else files method = method.upper() if method not in ALLOWED_METHODS: raise UrlfetchException("Method should be one of " + ", ".join(ALLOWED_METHODS)) if params: if isinstance(params, dict): url = url_concat(url, params) elif isinstance(params, (str, bytes)): if url[-1] not in ("?", "&"): url += "&" if ("?" in url) else "?" url += params parsed_url = parse_url(url) reqheaders = { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "User-Agent": random_useragent(randua), "Host": parsed_url["http_host"], } # Proxy support if proxies is None and trust_env: proxies = PROXIES if not proxies: proxies = {} ignore_hosts = _no_proxy_hosts(trust_env) if not auth and parsed_url["username"] and parsed_url["password"]: auth = (parsed_url["username"], parsed_url["password"]) if auth: if isinstance(auth, (list, tuple)): auth = "%s:%s" % tuple(auth) auth = base64.b64encode(auth.encode("utf-8")) reqheaders["Authorization"] = "Basic " + auth.decode("utf-8") if json is not None and (data is not None or files): raise UrlfetchException("json cannot be combined with data or files") if files: content_type, data = encode_multipart(data, files) reqheaders["Content-Type"] = content_type elif json is not None: data = json_dumps(json) reqheaders["Content-Type"] = "application/json" elif isinstance(data, dict): data = urlencode(data, True) if isinstance(data, (str, bytes)) and not files and "Content-Type" not in reqheaders: # httplib will set 'Content-Length', also you can set it by yourself reqheaders["Content-Type"] = "application/x-www-form-urlencoded" # what if the method is GET, HEAD or DELETE # just do not make so much decisions for users _merge_headers(reqheaders, headers) start_time = time.time() conn, resp = perform(url, method, data, reqheaders, parsed_url) end_time = time.time() total_time = end_time - start_time history = [] response = Response.from_httplib( resp, connection=conn, reqheaders=dict(reqheaders), length_limit=length_limit, history=history[:], url=url, total_time=total_time, start_time=start_time, ) while ( response.status in REDIRECT_STATUSES and "location" in response.headers and max_redirects ): response.body, response.close(), history.append(response) if len(history) > max_redirects: raise TooManyRedirects("max_redirects exceeded") if response.status not in PRESERVE_METHOD_REDIRECTS: method = "GET" data = None reqheaders.pop("Content-Type", None) reqheaders.pop("Content-Length", None) location = response.headers["location"] prev_origin = _origin(parsed_url) if location[:2] == "//": url = parsed_url["scheme"] + ":" + location else: url = urljoin(url, location) parsed_url = parse_url(url) reqheaders["Host"] = parsed_url["http_host"] reqheaders["Referer"] = response.url if prev_origin != _origin(parsed_url): reqheaders.pop("Authorization", None) reqheaders.pop("Cookie", None) conn, resp = perform(url, method, data, reqheaders, parsed_url) response = Response.from_httplib( resp, connection=conn, reqheaders=dict(reqheaders), length_limit=length_limit, history=history[:], url=url, total_time=total_time, start_time=start_time, ) return response
############################################################################## # Shortcuts and Helpers ####################################################### ############################################################################## def _partial_method(method): func = partial(request, method=method) func.__doc__ = "Issue a %s request" % method.lower() func.__name__ = method.lower() func.__module__ = request.__module__ return func get = _partial_method(GET) post = _partial_method(POST) put = _partial_method(PUT) delete = _partial_method(DELETE) head = _partial_method(HEAD) options = _partial_method(OPTIONS) trace = _partial_method(TRACE) patch = _partial_method(PATCH) del _partial_method class ObjectDict(dict): """Makes a dictionary behave like an object.""" def __getattr__(self, name): try: return self[name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): self[name] = value
[docs] def parse_url(url): """Return a dictionary of parsed url Including scheme, netloc, path, params, query, fragment, uri, username, password, host, port and http_host """ try: url = str(url) except UnicodeDecodeError: pass if "://" in url: scheme, url = url.split("://", 1) else: scheme = "http" url = "http://" + url parsed = urlsplit(url) r = ObjectDict() r["scheme"] = scheme r["netloc"] = parsed.netloc r["path"] = parsed.path r["query"] = parsed.query r["fragment"] = parsed.fragment r["uri"] = parsed.path if parsed.query: r["uri"] += "?" + parsed.query r["username"] = parsed.username r["password"] = parsed.password host = parsed.hostname.encode("idna").decode("utf-8") r["host"] = r["hostname"] = host try: r["port"] = parsed.port except ValueError: r["port"] = None if ":" in r["host"]: http_host = "[%s]" % r["host"] else: http_host = r["host"] if r["port"]: r["http_host"] = "%s:%d" % (http_host, r["port"]) else: r["http_host"] = http_host return r
[docs] def get_proxies_from_environ(): """Get proxies from os.environ.""" proxies = {} http_proxy = os.getenv("http_proxy") or os.getenv("HTTP_PROXY") https_proxy = os.getenv("https_proxy") or os.getenv("HTTPS_PROXY") if http_proxy: proxies["http"] = http_proxy if https_proxy: proxies["https"] = https_proxy return proxies
[docs] def mb_code(s, coding=None, errors="replace"): """encoding/decoding helper.""" if isinstance(s, str): return s if coding is None else s.encode(coding, errors=errors) for c in ("utf-8", "gb2312", "gbk", "gb18030", "big5"): try: s = s.decode(c) return s if coding is None else s.encode(coding, errors=errors) except (UnicodeDecodeError, LookupError): pass return str(s, errors=errors)
[docs] def random_useragent(filename=True): """Returns a User-Agent string randomly from file. :arg string filename: (Optional) Path to the file from which a random useragent is generated. By default it's ``True``, a file shipped with this module will be used. :returns: An user-agent string. """ import random default_ua = "urlfetch/%s" % __version__ if isinstance(filename, (str, bytes)): filenames = [filename] else: filenames = [] if filename and UAFILE: filenames.append(UAFILE) for filename in filenames: try: st = os.stat(filename) if stat.S_ISREG(st.st_mode) and os.access(filename, os.R_OK): break except (OSError, AttributeError): pass else: return default_ua with open(filename, "rb") as f: filesize = st.st_size pos = 0 r = random.Random() # try getting a valid line for no more than 3 times for i in range(3): pos += r.randint(0, filesize) pos %= filesize f.seek(pos) # in case we are in middle of a line f.readline() line = f.readline() if not line: if f.tell() == filesize: # end of file f.seek(0) line = f.readline() line = line.strip() if line and line[0] != "#": return line return default_ua
[docs] def url_concat(url, args, keep_existing=True): """Concatenate url and argument dictionary >>> url_concat("http://example.com/foo?a=b", dict(c="d")) 'http://example.com/foo?a=b&c=d' :arg string url: URL being concat to. :arg dict args: Args being concat. :arg bool keep_existing: (Optional) Whether to keep the args which are alreay in url, default is ``True``. """ if not args: return url if keep_existing: if url[-1] not in ("?", "&"): url += "&" if ("?" in url) else "?" return url + urlencode(args, True) else: url, seq, query = url.partition("?") query = parse_qs(query, True) query.update(args) return url + "?" + urlencode(query, True)
def _multipart_param(name, value): value = ( str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\r", " ").replace("\n", " ") ) return '%s="%s"' % (name, value)
[docs] def choose_boundary(): """Generate a multipart boundry. :returns: A boundary string """ global BOUNDARY_PREFIX if BOUNDARY_PREFIX is None: BOUNDARY_PREFIX = "urlfetch" try: uid = repr(os.getuid()) BOUNDARY_PREFIX += "." + uid except AttributeError: pass try: pid = repr(os.getpid()) BOUNDARY_PREFIX += "." + pid except AttributeError: pass return "%s.%s" % (BOUNDARY_PREFIX, uuid.uuid4().hex)
[docs] def encode_multipart(data, files): """Encode multipart. :arg dict data: Data to be encoded :arg dict files: Files to be encoded :returns: Encoded binary string :raises: :class:`UrlfetchException` """ body = BytesIO() boundary = choose_boundary() part_boundary = ("--%s\r\n" % boundary).encode("ascii") writer = codecs.lookup("utf-8")[3] if isinstance(data, dict): for name, values in data.items(): if not isinstance(values, (list, tuple, set)): # behave like urllib.urlencode(dict, True) values = (values,) for value in values: body.write(part_boundary) writer(body).write( "Content-Disposition: form-data; %s\r\n" % _multipart_param("name", name) ) body.write(b"Content-Type: text/plain\r\n\r\n") if isinstance(value, int): value = str(value) if isinstance(value, str): writer(body).write(value) else: body.write(value) body.write(b"\r\n") for fieldname, f in files.items(): ctype = None if isinstance(f, tuple): if len(f) == 2: filename, f = f elif len(f) >= 3: filename, f, ctype = f[0], f[1], f[2] else: raise UrlfetchException("file must has filename") elif hasattr(f, "name"): filename = basename(f.name) else: filename = None raise UrlfetchException("file must has filename") if hasattr(f, "read"): value = f.read() elif isinstance(f, (str, bytes)): value = f else: value = str(f) body.write(part_boundary) if filename: writer(body).write( "Content-Disposition: form-data; %s; %s\r\n" % ( _multipart_param("name", fieldname), _multipart_param("filename", filename), ) ) ctype = ( ctype or mimetypes.guess_type(str(filename))[0] or "application/octet-stream" ) body.write(b"Content-Type: %s\r\n\r\n" % ctype.encode("ascii", "replace")) else: writer(body).write( "Content-Disposition: form-data; %s\r\n" % _multipart_param("name", fieldname) ) body.write(b"Content-Type: text/plain\r\n\r\n") if isinstance(value, str): writer(body).write(value) else: body.write(value) body.write(b"\r\n") body.write(("--%s--\r\n" % boundary).encode("ascii")) content_type = "multipart/form-data; boundary=%s" % boundary return content_type, body.getvalue()
############################################################################## # Constants and Globals ####################################################### ############################################################################## ALLOWED_METHODS = (GET, DELETE, HEAD, OPTIONS, PUT, POST, TRACE, PATCH) PROXY_IGNORE_HOSTS = ("127.0.0.1", "localhost") PROXIES = get_proxies_from_environ() BOUNDARY_PREFIX = None UAFILENAME = "urlfetch.useragents.list" UAFILE = next( ( i for i in set( ( pathjoin(sys.prefix, UAFILENAME), pathjoin(sys.prefix, "local", UAFILENAME), pathjoin(dirname(abspath(__file__)), UAFILENAME), ) ) if os.path.isfile(i) ), None, )