zoukankan      html  css  js  c++  java
  • HttpRunner3源码阅读:6.请求客户端client

    client

    上一篇读了parser.py,处理语法$var,${func($var1)}变量、函数的文件,这次来看client.py看名字就差不多明白了 请求方法封装

    导包

    import json  # 内置json库
    import time  # 时间处理
    
    import requests  # 第三方请求库 requests
    import urllib3  # 内置的 urllib3 requests库基于它实现
    from loguru import logger  # 日志库
    from requests import Request, Response  # 导入 请求类、响应类
    from requests.exceptions import (  # 一些异常类
        InvalidSchema,
        InvalidURL,
        MissingSchema,
        RequestException,
    )
    
    from httprunner.models import RequestData, ResponseData
    from httprunner.models import SessionData, ReqRespData
    from httprunner.utils import lower_dict_keys, omit_long_data  # key转换小写, 长度处理
    

    RequestsData & ResponseData

    这里把之前的模型类复制过来

    # 请求
    class RequestData(BaseModel):
        method: MethodEnum = MethodEnum.GET
        url: Url
        headers: Headers = {}
        cookies: Cookies = {}
        body: Union[Text, bytes, List, Dict, None] = {}
    
    # 响应
    class ResponseData(BaseModel):
        status_code: int
        headers: Dict
        cookies: Cookies
        encoding: Union[Text, None] = None
        content_type: Text
        body: Union[Text, bytes, List, Dict]
    

    源码附注释

    # 禁用InsecureRequestWarning 
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    # 继承Response 类 ,重写 raise_for_status 状态码异常方法
    class ApiResponse(Response):
        def raise_for_status(self):
            if hasattr(self, "error") and self.error:
                raise self.error
            Response.raise_for_status(self)
    
    # 从响应对象获取到请求和响应信息
    def get_req_resp_record(resp_obj: Response) -> ReqRespData:
        """ get request and response info from Response() object.
        """
    
        def log_print(req_or_resp, r_type): 
            msg = f"
    ================== {r_type} details ==================
    "
            for key, value in req_or_resp.dict().items():
                if isinstance(value, dict) or isinstance(value, list):
                    value = json.dumps(value, indent=4, ensure_ascii=False)
    
                msg += "{:<8} : {}
    ".format(key, value)
            logger.debug(msg)
    
        # record actual request info  # 响应对象拿到请求header
        request_headers = dict(resp_obj.request.headers)
        request_cookies = resp_obj.request._cookies.get_dict() # 拿到请求cookies
    
        request_body = resp_obj.request.body  # 拿到请求体
        if request_body is not None:
            try:
                request_body = json.loads(request_body)
            except json.JSONDecodeError:
                # str: a=1&b=2
                pass
            except UnicodeDecodeError:
                # bytes/bytearray: request body in protobuf
                pass
            except TypeError:
                # neither str nor bytes/bytearray, e.g. <MultipartEncoder>
                pass
    
            request_content_type = lower_dict_keys(request_headers).get("content-type") 
            if request_content_type and "multipart/form-data" in request_content_type:
                # upload file type  如果是上传文件 就改请求体内容
                request_body = "upload file stream (OMITTED)"
        # 实例RequestData模型
        request_data = RequestData(
            method=resp_obj.request.method,
            url=resp_obj.request.url,
            headers=request_headers,
            cookies=request_cookies,
            body=request_body,
        )
    
        # log request details in debug mode
        log_print(request_data, "request")
    
        # record response info
        resp_headers = dict(resp_obj.headers)  # 响应头
        lower_resp_headers = lower_dict_keys(resp_headers)
        content_type = lower_resp_headers.get("content-type", "")
    
        if "image" in content_type:
            # response is image type, record bytes content only
            response_body = resp_obj.content  # 二进制内容获取
        else:
            try:
                # try to record json data
                response_body = resp_obj.json()  # 响应结果
            except ValueError:
                # only record at most 512 text charactors
                resp_text = resp_obj.text
                response_body = omit_long_data(resp_text)  # 长度处理
    
        # 实例化ResponseData模型
        response_data = ResponseData(
            status_code=resp_obj.status_code,
            cookies=resp_obj.cookies or {},
            encoding=resp_obj.encoding,
            headers=resp_headers,
            content_type=content_type,
            body=response_body,
        )
    
        # log response details in debug mode
        log_print(response_data, "response")
        
        # 实例化ReqRespData 其就是 RequestData ResponseData 组成
        req_resp_data = ReqRespData(request=request_data, response=response_data)
        return req_resp_data
    
    # 继承requests.Session
    class HttpSession(requests.Session):
        """
        Class for performing HTTP requests and holding (session-) cookies between requests (in order
        to be able to log in and out of websites). Each request is logged so that HttpRunner can
        display statistics.
    
        This is a slightly extended version of `python-request <http://python-requests.org>`_'s
        :py:class:`requests.Session` class and mostly this class works exactly the same.
        """
    
        def __init__(self):
            # 调用父类构造方法
            super(HttpSession, self).__init__()
            # 实例SessionData模型
            self.data = SessionData()
    
        def update_last_req_resp_record(self, resp_obj):  # 更新响应对象
            """
            update request and response info from Response() object.
            """
            # TODO: fix
            self.data.req_resps.pop()
            self.data.req_resps.append(get_req_resp_record(resp_obj))
    
        def request(self, method, url, name=None, **kwargs): # 请求方法
            """
            Constructs and sends a :py:class:`requests.Request`.
            Returns :py:class:`requests.Response` object.
    
            :param method:
                method for the new :class:`Request` object.
            :param url:
                URL for the new :class:`Request` object.
            :param name: (optional)
                Placeholder, make compatible with Locust's HttpSession
            :param params: (optional)
                Dictionary or bytes to be sent in the query string for the :class:`Request`.
            :param data: (optional)
                Dictionary or bytes to send in the body of the :class:`Request`.
            :param headers: (optional)
                Dictionary of HTTP Headers to send with the :class:`Request`.
            :param cookies: (optional)
                Dict or CookieJar object to send with the :class:`Request`.
            :param files: (optional)
                Dictionary of ``'filename': file-like-objects`` for multipart encoding upload.
            :param auth: (optional)
                Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth.
            :param timeout: (optional)
                How long to wait for the server to send data before giving up, as a float, or 
                a (`connect timeout, read timeout <user/advanced.html#timeouts>`_) tuple.
                :type timeout: float or tuple
            :param allow_redirects: (optional)
                Set to True by default.
            :type allow_redirects: bool
            :param proxies: (optional)
                Dictionary mapping protocol to the URL of the proxy.
            :param stream: (optional)
                whether to immediately download the response content. Defaults to ``False``.
            :param verify: (optional)
                if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
            :param cert: (optional)
                if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
            """
            self.data = SessionData()
    
            # timeout default to 120 seconds
            kwargs.setdefault("timeout", 120)
    
            # set stream to True, in order to get client/server IP/Port
            kwargs["stream"] = True
    
            start_timestamp = time.time()  # 计时
            response = self._send_request_safe_mode(method, url, **kwargs)
            response_time_ms = round((time.time() - start_timestamp) * 1000, 2)  # 算时间
    
            try: # 拿客户端数据,确实现在才知道这种操作 
                client_ip, client_port = response.raw.connection.sock.getsockname()
                self.data.address.client_ip = client_ip
                self.data.address.client_port = client_port
                logger.debug(f"client IP: {client_ip}, Port: {client_port}")
            except AttributeError as ex:
                logger.warning(f"failed to get client address info: {ex}")
    
            try:
                # 拿服务端数据
                server_ip, server_port = response.raw.connection.sock.getpeername()
                self.data.address.server_ip = server_ip
                self.data.address.server_port = server_port
                logger.debug(f"server IP: {server_ip}, Port: {server_port}")
            except AttributeError as ex:
                logger.warning(f"failed to get server address info: {ex}")
    
            # get length of the response content
            content_size = int(dict(response.headers).get("content-length") or 0)
    
            # record the consumed time
            self.data.stat.response_time_ms = response_time_ms  
            self.data.stat.elapsed_ms = response.elapsed.microseconds / 1000.0 # 响应时间
            self.data.stat.content_size = content_size
    
            # record request and response histories, include 30X redirection
            response_list = response.history + [response]
            self.data.req_resps = [
                get_req_resp_record(resp_obj) for resp_obj in response_list
            ]
    
            try:
                response.raise_for_status()
            except RequestException as ex:
                logger.error(f"{str(ex)}")
            else:
                logger.info(
                    f"status_code: {response.status_code}, "
                    f"response_time(ms): {response_time_ms} ms, "
                    f"response_length: {content_size} bytes"
                )
    
            return response
    
        def _send_request_safe_mode(self, method, url, **kwargs):
            """
            Send a HTTP request, and catch any exception that might occur due to connection problems.
            Safe mode has been removed from requests 1.x.
            """
            try:
                return requests.Session.request(self, method, url, **kwargs)
            except (MissingSchema, InvalidSchema, InvalidURL):
                raise
            except RequestException as ex:
                resp = ApiResponse()
                resp.error = ex
                resp.status_code = 0  # with this status_code, content returns None
                resp.request = Request(method, url).prepare()  # "Constructs a :class:`PreparedRequest <PreparedRequest>` for transmission and returns it.
                return resp
    
    
    作者:zy7y
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    centos yum安装与配置vsFTPd FTP服务器(转)
    CentOS 6.9安装配置nmon
    Linux 中将用户添加到组的指令
    linux 设置用户组共享文件
    linux时间同步ntpdate
    java获取端口号,不用request
    centos6.5 yum安装redis
    Centos6 yum安装nginx
    Nginx 不支持WebSocket TCP
    django中的分页应用 django-pure-pagination
  • 原文地址:https://www.cnblogs.com/zy7y/p/15104775.html
Copyright © 2011-2022 走看看