client.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import json
  2. from collections.abc import Generator
  3. from typing import Union
  4. from urllib.parse import urljoin
  5. import requests
  6. from requests import Response
  7. from core.rag.extractor.watercrawl.exceptions import (
  8. WaterCrawlAuthenticationError,
  9. WaterCrawlBadRequestError,
  10. WaterCrawlPermissionError,
  11. )
  12. class BaseAPIClient:
  13. def __init__(self, api_key, base_url):
  14. self.api_key = api_key
  15. self.base_url = base_url
  16. self.session = self.init_session()
  17. def init_session(self):
  18. session = requests.Session()
  19. session.headers.update({"X-API-Key": self.api_key})
  20. session.headers.update({"Content-Type": "application/json"})
  21. session.headers.update({"Accept": "application/json"})
  22. session.headers.update({"User-Agent": "WaterCrawl-Plugin"})
  23. session.headers.update({"Accept-Language": "en-US"})
  24. return session
  25. def _get(self, endpoint: str, query_params: dict | None = None, **kwargs):
  26. return self.session.get(urljoin(self.base_url, endpoint), params=query_params, **kwargs)
  27. def _post(self, endpoint: str, query_params: dict | None = None, data: dict | None = None, **kwargs):
  28. return self.session.post(urljoin(self.base_url, endpoint), params=query_params, json=data, **kwargs)
  29. def _put(self, endpoint: str, query_params: dict | None = None, data: dict | None = None, **kwargs):
  30. return self.session.put(urljoin(self.base_url, endpoint), params=query_params, json=data, **kwargs)
  31. def _delete(self, endpoint: str, query_params: dict | None = None, **kwargs):
  32. return self.session.delete(urljoin(self.base_url, endpoint), params=query_params, **kwargs)
  33. def _patch(self, endpoint: str, query_params: dict | None = None, data: dict | None = None, **kwargs):
  34. return self.session.patch(urljoin(self.base_url, endpoint), params=query_params, json=data, **kwargs)
  35. class WaterCrawlAPIClient(BaseAPIClient):
  36. def __init__(self, api_key, base_url: str | None = "https://app.watercrawl.dev/"):
  37. super().__init__(api_key, base_url)
  38. def process_eventstream(self, response: Response, download: bool = False) -> Generator:
  39. for line in response.iter_lines():
  40. line = line.decode("utf-8")
  41. if line.startswith("data:"):
  42. line = line[5:].strip()
  43. data = json.loads(line)
  44. if data["type"] == "result" and download:
  45. data["data"] = self.download_result(data["data"])
  46. yield data
  47. def process_response(self, response: Response) -> dict | bytes | list | None | Generator:
  48. if response.status_code == 401:
  49. raise WaterCrawlAuthenticationError(response)
  50. if response.status_code == 403:
  51. raise WaterCrawlPermissionError(response)
  52. if 400 <= response.status_code < 500:
  53. raise WaterCrawlBadRequestError(response)
  54. response.raise_for_status()
  55. if response.status_code == 204:
  56. return None
  57. if response.headers.get("Content-Type") == "application/json":
  58. return response.json() or {}
  59. if response.headers.get("Content-Type") == "application/octet-stream":
  60. return response.content
  61. if response.headers.get("Content-Type") == "text/event-stream":
  62. return self.process_eventstream(response)
  63. raise Exception(f"Unknown response type: {response.headers.get('Content-Type')}")
  64. def get_crawl_requests_list(self, page: int | None = None, page_size: int | None = None):
  65. query_params = {"page": page or 1, "page_size": page_size or 10}
  66. return self.process_response(
  67. self._get(
  68. "/api/v1/core/crawl-requests/",
  69. query_params=query_params,
  70. )
  71. )
  72. def get_crawl_request(self, item_id: str):
  73. return self.process_response(
  74. self._get(
  75. f"/api/v1/core/crawl-requests/{item_id}/",
  76. )
  77. )
  78. def create_crawl_request(
  79. self,
  80. url: Union[list, str] | None = None,
  81. spider_options: dict | None = None,
  82. page_options: dict | None = None,
  83. plugin_options: dict | None = None,
  84. ):
  85. data = {
  86. # 'urls': url if isinstance(url, list) else [url],
  87. "url": url,
  88. "options": {
  89. "spider_options": spider_options or {},
  90. "page_options": page_options or {},
  91. "plugin_options": plugin_options or {},
  92. },
  93. }
  94. return self.process_response(
  95. self._post(
  96. "/api/v1/core/crawl-requests/",
  97. data=data,
  98. )
  99. )
  100. def stop_crawl_request(self, item_id: str):
  101. return self.process_response(
  102. self._delete(
  103. f"/api/v1/core/crawl-requests/{item_id}/",
  104. )
  105. )
  106. def download_crawl_request(self, item_id: str):
  107. return self.process_response(
  108. self._get(
  109. f"/api/v1/core/crawl-requests/{item_id}/download/",
  110. )
  111. )
  112. def monitor_crawl_request(self, item_id: str, prefetched=False) -> Generator:
  113. query_params = {"prefetched": str(prefetched).lower()}
  114. generator = self.process_response(
  115. self._get(f"/api/v1/core/crawl-requests/{item_id}/status/", stream=True, query_params=query_params),
  116. )
  117. if not isinstance(generator, Generator):
  118. raise ValueError("Generator expected")
  119. yield from generator
  120. def get_crawl_request_results(
  121. self, item_id: str, page: int = 1, page_size: int = 25, query_params: dict | None = None
  122. ):
  123. query_params = query_params or {}
  124. query_params.update({"page": page or 1, "page_size": page_size or 25})
  125. return self.process_response(
  126. self._get(f"/api/v1/core/crawl-requests/{item_id}/results/", query_params=query_params)
  127. )
  128. def scrape_url(
  129. self,
  130. url: str,
  131. page_options: dict | None = None,
  132. plugin_options: dict | None = None,
  133. sync: bool = True,
  134. prefetched: bool = True,
  135. ):
  136. response_result = self.create_crawl_request(url=url, page_options=page_options, plugin_options=plugin_options)
  137. if not sync:
  138. return response_result
  139. for event_data in self.monitor_crawl_request(response_result["uuid"], prefetched):
  140. if event_data["type"] == "result":
  141. return event_data["data"]
  142. def download_result(self, result_object: dict):
  143. response = requests.get(result_object["result"])
  144. response.raise_for_status()
  145. result_object["result"] = response.json()
  146. return result_object