Coverage for src/integry/resources/base.py: 75%
61 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-03-26 01:28 +0500
« prev ^ index » next coverage.py v7.6.1, created at 2025-03-26 01:28 +0500
1from typing import AsyncIterator, Awaitable, Literal, Type, TypeVar
2import httpx
3from pydantic import BaseModel
5from integry.exceptions import NotFound
6from integry.utils.common import get_hash
9class BaseResource:
10 http_client: httpx.AsyncClient
11 app_key: str
12 app_secret: str
13 name: Literal["functions"] | Literal["apps"]
15 def __init__(
16 self,
17 client: httpx.AsyncClient,
18 app_key: str,
19 app_secret: str,
20 resource: Literal["functions"] | Literal["apps"],
21 ):
22 self.http_client = client
23 self.app_key = app_key
24 self.app_secret = app_secret
25 self.name = resource
27 def _get_response_data_or_raise(self, response: httpx.Response):
28 if response.status_code == 404: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 data = response.json()
30 raise NotFound(data.get("detail"))
32 if response.status_code != 200: 32 ↛ 33line 32 didn't jump to line 33 because the condition on line 32 was never true
33 raise Exception(response.content)
35 return response.json()
37 def _get_signed_request_headers(self, user_id: str):
38 hash = get_hash(self.app_secret, user_id)
39 return {"App-Key": self.app_key, "User-ID": user_id, "Hash": hash}
42ResourceModel = TypeVar("ResourceModel", bound=BaseModel)
43ResourcePage = TypeVar("ResourcePage", bound=BaseModel)
46class AsyncPaginator(AsyncIterator[ResourceModel], Awaitable[ResourcePage]):
47 _cursor_key = "_cursor"
49 def __init__(
50 self,
51 resource: BaseResource,
52 user_id: str,
53 query_string: str,
54 model: Type[ResourceModel],
55 paginated_response_model: Type[ResourcePage],
56 explicit_cursor: str = "",
57 ):
58 self._resource = resource
59 self._user_id = user_id
61 self._query_string = query_string
62 self._data = []
63 self._cursor = ""
64 self._explicit_cursor = explicit_cursor
66 self._model = model
67 self._paginated_response_model = paginated_response_model
69 def __aiter__(self):
70 return self
72 async def __anext__(self):
73 if not self._data and self._cursor is not None:
74 await self._populate_next_page()
76 if not self._data:
77 raise StopAsyncIteration
79 return self._data.pop(0)
81 def __await__(self):
82 return self._get_data().__await__()
84 async def _populate_next_page(self):
85 self._data, self._cursor = await self._get_next_page(self._cursor)
87 async def _get_data(self) -> ResourcePage:
88 data, cursor = await self._get_next_page(self._explicit_cursor)
90 if cursor is None: 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true
91 cursor = ""
93 return self._paginated_response_model(
94 cursor=cursor, **{self._resource.name: data}
95 )
97 async def _get_next_page(self, cursor) -> tuple[list[ResourceModel], str | None]:
98 response = await self._resource.http_client.post(
99 f"{self._resource.name}/list/{self._query_string}",
100 headers=self._resource._get_signed_request_headers(self._user_id),
101 # json={self._cursor_key: cursor},
102 )
103 response_data = self._resource._get_response_data_or_raise(response)
105 data = [
106 self._model(**record, _resource=self._resource)
107 for record in response_data.get(self._resource.name, [])
108 ]
109 next_cursor = response_data.get(self._cursor_key)
110 return data, next_cursor