Coverage for src\zapy\requests\requester.py: 88%

1 from typing import List

2 from dataclasses import dataclass, field

3 import inspect

4 import asyncio

5

6 import httpx

7

8 from zapy.base import Metadata

9 from zapy.test import run_tests, TestResult

10 from zapy.store import Store, use_store

11 from zapy.utils import functools

12 from zapy.templating.traceback import annotate_traceback

13

14 from .exceptions import error_location, RenderLocationException

15 from .models import ZapyRequest, HttpxArguments

16 from .context import ZapyRequestContext

17 from .hooks import use_global_hook

18 from .converter import RequestConverter

19

20

21@dataclass

22 class RequesterResponse:

23 response: httpx.Response

24 test_result: List[TestResult] = field(default_factory=list)

25

26 _http_request_signature = inspect.signature(httpx.Client.build_request).parameters

27

28

29 class Requester:

30

31 def __init__(self, zapy_request: ZapyRequest, converter: RequestConverter, client: httpx.AsyncClient):

32 self.zapy_request = zapy_request

33 self.converter = converter

34 self.client = client

35 self.__hook_context = {

36 Metadata: self.zapy_request.metadata,

37 ZapyRequest: self.zapy_request,

38 }

39

40 async def make_request(self) -> RequesterResponse:

41 httpx_args = self.converter.build_httpx_args()

42

43 # Hook: pre_request

44 await self._invoke_hooks_pre_request(httpx_args)

45

46 # send request

47 request_parameters, rest_parameters = self._split_parameters(httpx_args)

48 request = self.client.build_request(**request_parameters)

49 response = await self.client.send(request, **rest_parameters)

50

51 # Hook: post_request

52 try:

53 await self._invoke_hooks_post_request(response)

54 except RenderLocationException as ex:

55 ex.context["response"] = response

56 raise

57

58 response_wrapper = RequesterResponse(response)

59

60 # Hook: test

61 if self.request_hooks.test:

62 response_wrapper.test_result = self._run_test(

63 httpx_args = httpx_args,

64 request = request,

65 response = response,

66 )

67

68 return response_wrapper

69

70 @error_location('pre_request')

71 async def _invoke_hooks_pre_request(self, httpx_args: dict):

72 try:

73 await self.__call_hook(use_global_hook().pre_request, httpx_args)

74 await self.__call_hook(self.request_hooks.pre_request, httpx_args)

75 except BaseException as e:

76 annotate_traceback(e, self.converter.script, location='hook')

77 raise e

78

79 @error_location('post_request')

80 async def _invoke_hooks_post_request(self, response: httpx.Response):

81 try:

82 await self.__call_hook(use_global_hook().post_request, response)

83 await self.__call_hook(self.request_hooks.post_request, response)

84 except Exception as e:

85 annotate_traceback(e, self.converter.script, location='hook')

86 raise e

87

88 def _run_test(self, **args) -> dict:

89 class RequestMeta(type):

90 def __new__(cls, name, bases, attrs):

91 for k, v in args.items():

92 attrs[k] = v

93 return super().__new__(cls, name, bases, attrs)

94 class DecoratedClass(self.request_hooks.test, metaclass=RequestMeta):

95 pass

96

97 test_result = run_tests(DecoratedClass).as_list()

98 return test_result

99

100 def _split_parameters(self, httpx_args: HttpxArguments):

101 request_parameters, rest_parameters = dict(), dict()

102 for k, v in httpx_args.items():

103 if k in _http_request_signature: 103 ↛ 106line 103 didn't jump to line 106, because the condition on line 103 was never false

104 request_parameters[k] = v

105 else:

106 rest_parameters[k] = v

107 return request_parameters, rest_parameters

108

109 async def __call_hook(self, hook, *args):

110 result = functools.call_with_signature(hook, *args, kwargs=self.__hook_context)

111 if asyncio.iscoroutine(result): 111 ↛ 113line 111 didn't jump to line 113, because the condition on line 111 was never false

112 return await result

113 return result

114

115 @property

116 def request_hooks(self):

117 return self.converter.request_hooks

118

119 async def send_request(zapy_request: ZapyRequest, *, store: Store | None = None, logger=print, client: httpx.AsyncClient | None =None) -> RequesterResponse:

120 if store is None:

121 store = use_store()

122 ctx = ZapyRequestContext(

123 store = store,

124 logger = logger,

125 )

126 _client = client or httpx.AsyncClient(follow_redirects=True, timeout=None)

127 try:

128 request = build_request(zapy_request, ctx, client=_client)

129 return await request.make_request()

130 finally:

131 if client is None: 131 ↛ exitline 131 didn't return from function 'send_request', because the return on line 129 wasn't executed

132 await _client.aclose()

133

134 def build_request(zapy_request: ZapyRequest, ctx: ZapyRequestContext, client: httpx.AsyncClient) -> Requester:

135 converter = RequestConverter(zapy_request, ctx)

136 requester = Requester(zapy_request, converter, client)

137

138 return requester