From e04bdfb1998b445fd8abb09b93892a0e9eb67d7e Mon Sep 17 00:00:00 2001 From: Estelle Poulin Date: Sat, 18 Nov 2023 16:46:46 -0500 Subject: [PATCH] Add async support --- CHANGELOG.md | 21 ++++ README.md | 19 ++++ kaginawa/__init__.py | 2 +- kaginawa/async_client.py | 206 +++++++++++++++++++++++++++++++++++++++ kaginawa/client.py | 22 +++-- requirements.txt | 2 +- 6 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 kaginawa/async_client.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a0cf709..942a48a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,22 @@ # CHANGELOG + +# 0.0.8 + +Async Support! + +* We now use the `httpx` package to provide sync and asyn support. + +```python +import asyncio + +from kaginawa import AsyncKaginawa + + +async def amain(): + kagi_client = AsyncKaginawa(...) + res = await kagi_client.generate(...) + print(res.output) + +if __name__ == "__main__": + asyncio.run(amain()) +``` diff --git a/README.md b/README.md index 4fcb72d..38bc5f9 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,25 @@ response: KaginawaSummarizationResponse = client.summarize( print(response.output) ``` + +## Async! + +```python +import asyncio +from kaginawa.async_client import AsyncKaginawa + +async def amain(): + kagi_client = AsyncKaginawa(...) + res = await kagi_client.generate(...) + print(res.output) + + # If you want to explicitly close the client. + kagi_client.close() + +if __name__ == "__main__": + asyncio.run(amain()) +``` + ## FAQ
diff --git a/kaginawa/__init__.py b/kaginawa/__init__.py index ba88f37..dfe1ade 100644 --- a/kaginawa/__init__.py +++ b/kaginawa/__init__.py @@ -1,3 +1,3 @@ from pkg_resources import parse_version -__version__ = parse_version("0.0.7") +__version__ = parse_version("0.0.8") diff --git a/kaginawa/async_client.py b/kaginawa/async_client.py new file mode 100644 index 0000000..0093541 --- /dev/null +++ b/kaginawa/async_client.py @@ -0,0 +1,206 @@ +import json +import os +from typing import Optional + +import httpx + +from kaginawa.exceptions import KaginawaError +from kaginawa.models import ( + KaginawaEnrichResponse, + KaginawaFastGPTResponse, + KaginawaSummarizationResponse, +) + + +class AsyncKaginawa: + """The main client to the Kagi API""" + + def __init__( + self, + token: Optional[str] = None, + session: Optional[httpx.AsyncClient] = None, + api_base: str = "https://kagi.com/api", + ): + """Create a new instance of the Kagi API wrapper. + + Parameters: + token (Optional[str], optional): The API access token to authenticate + httpx. If this value is unset the library will attempt to read the + environment variable KAGI_API_KEY. If both are unset an exception will + be raised. + + session (Optional[httpx.Client], optional): An optional `httpx` + session object to use for sending HTTP httpx. Defaults to `None`. + + api_base (str, optional): The base URL for the Kagi API endpoint. + Defaults to "https://kagi.com/api". + """ + + try: + token = token or os.environ["KAGI_API_KEY"] + except KeyError as e: + raise KaginawaError("Value for `token` not given and env KAGI_API_KEY is unset") from e + + self.token = token + self.api_base = api_base + + if not session: + session = httpx.AsyncClient() + + self.session = session + + self.session.headers = {"Authorization": f"Bot {self.token}"} + + async def close(self): + return await self.session.aclose() + + async def generate(self, query: str, cache: Optional[bool] = None): + """Generate a FastGPT response from a text query. + + Parameters: + query (str): The prompt to send to FastGPT. + + cache (Optional[bool], optional): Allow cached responses. Defaults to `None`. + + Returns: + KaginawaFastGPTResponse: A model representing the response. + """ + try: + optional_params = {} + + if cache is not None: + optional_params["cache"] = cache + + res = await self.session.post( + f"{self.api_base}/v0/fastgpt", + json={ + "query": query, + **optional_params, + }, + ) + + res.raise_for_status() + + raw_response = res.json() + print(json.dumps(raw_response, indent=2, sort_keys=True)) + except httpx.HTTPError as e: + raise KaginawaError("Error calling /v0/fastgpt") from e + + return KaginawaFastGPTResponse.from_raw(raw_response) + + async def enrich_web(self, query: str): + """Query the Teclis index for relevant web results for a given query. + + Parameters: + query (str): The search query. + + Returns: + KaginawaEnrichWebResponse: A model representing the response. + """ + + try: + res = await self.session.get( + f"{self.api_base}/v0/enrich/web", + params={"q": query}, + ) + res.raise_for_status() + + raw_response = res.json() + except httpx.HTTPError as e: + raise KaginawaError("Error calling /v0/enrich/web") from e + + return KaginawaEnrichResponse.from_raw(raw_response).results[0] + + async def enrich_news(self, query: str): + """Query the Teclis index for relevant web results for a given query. + + Parameters: + query (str): The search query. + + Returns: + KaginawaEnrichWebResponse: A model representing the response. + """ + + try: + res = await self.session.get( + f"{self.api_base}/v0/enrich/news", + params={"q": query}, + ) + res.raise_for_status() + + raw_response = res.json() + except httpx.HTTPError as e: + raise KaginawaError("Error calling /v0/enrich/web") from e + + return KaginawaEnrichResponse.from_raw(raw_response).results[0] + + async def summarize( + self, + url: Optional[str] = None, + text: Optional[str] = None, + engine: Optional[str] = None, + summary_type: Optional[str] = None, + target_language: Optional[str] = None, + cache: Optional[bool] = None, + ): + """Summarize a URL or text snippet. + + Parameters: + url (Optional[str], optional): The URL to summarize. This option is exclusive + with the `text` parameter. Defaults to `None`. + + text (Optional[str], optional): The text snippet to summarize. This option is + exclusive with the `url` parameter. Defaults to `None`. + + engine (Optional[str], optional): The summarization engine to use. There is a + helper enum `KaginawaSummarizationEngine` with the valid values for this + parameter. Defaults to `None`. + + summary_type (Optional[str], optional): The 'kind' of summary you would like + the model to use. There is a helper enum KaginawaSummaryType with the valid + values for this parameter. Defaults to `None`. + + target_language: (Optional[str], optional): The language CODE (eg. EN, ZH, FR) + corresponding to the language you would like the summary in. See + https://help.kagi.com/kagi/api/summarizer.html for valid language codes. + Defaults to `None`. + + cache (Optional[bool], optional): Allow cached responses. Defaults to `None`. + + Returns: + KaginawaSummarizationResponse: A model representing the response. + """ + try: + params = {} + + if not (bool(url) ^ bool(text)): + raise KaginawaError("You must provide exactly one of 'url' or 'text'.") + + if url: + params["url"] = url + + if text: + params["text"] = text + + if engine: + params["engine"] = engine + + if summary_type: + params["summary_type"] = summary_type + + if target_language: + params["target_language"] = target_language + + if cache is not None: + params["cache"] = cache + + res = await self.session.post( + f"{self.api_base}/v0/summarize", + data=params, + ) + + raw_response = res.json() + except httpx.HTTPError as e: + raise KaginawaError("Error calling /v0/summarize") from e + + return KaginawaSummarizationResponse.from_raw(raw_response) diff --git a/kaginawa/client.py b/kaginawa/client.py index e5b7c7c..2d017bf 100644 --- a/kaginawa/client.py +++ b/kaginawa/client.py @@ -1,7 +1,8 @@ +import json import os from typing import Optional -import requests +import httpx from kaginawa.exceptions import KaginawaError from kaginawa.models import ( @@ -17,19 +18,19 @@ class Kaginawa: def __init__( self, token: Optional[str] = None, - session: Optional[requests.Session] = None, + session: Optional[httpx.Client] = None, api_base: str = "https://kagi.com/api", ): """Create a new instance of the Kagi API wrapper. Parameters: token (Optional[str], optional): The API access token to authenticate - requests. If this value is unset the library will attempt to read the + httpx. If this value is unset the library will attempt to read the environment variable KAGI_API_KEY. If both are unset an exception will be raised. - session (Optional[requests.Session], optional): An optional `requests` - session object to use for sending HTTP requests. Defaults to `None`. + session (Optional[httpx.Client], optional): An optional `httpx` + session object to use for sending HTTP httpx. Defaults to `None`. api_base (str, optional): The base URL for the Kagi API endpoint. Defaults to "https://kagi.com/api". @@ -44,7 +45,7 @@ def __init__( self.api_base = api_base if not session: - session = requests.Session() + session = httpx.Client() self.session = session @@ -78,7 +79,8 @@ def generate(self, query: str, cache: Optional[bool] = None): res.raise_for_status() raw_response = res.json() - except requests.RequestException as e: + print(json.dumps(raw_response, indent=2, sort_keys=True)) + except httpx.HTTPError as e: raise KaginawaError("Error calling /v0/fastgpt") from e return KaginawaFastGPTResponse.from_raw(raw_response) @@ -101,7 +103,7 @@ def enrich_web(self, query: str): res.raise_for_status() raw_response = res.json() - except requests.RequestException as e: + except httpx.HTTPError as e: raise KaginawaError("Error calling /v0/enrich/web") from e return KaginawaEnrichResponse.from_raw(raw_response).results[0] @@ -124,7 +126,7 @@ def enrich_news(self, query: str): res.raise_for_status() raw_response = res.json() - except requests.RequestException as e: + except httpx.HTTPError as e: raise KaginawaError("Error calling /v0/enrich/web") from e return KaginawaEnrichResponse.from_raw(raw_response).results[0] @@ -195,7 +197,7 @@ def summarize( ) raw_response = res.json() - except requests.RequestException as e: + except httpx.HTTPError as e: raise KaginawaError("Error calling /v0/summarize") from e return KaginawaSummarizationResponse.from_raw(raw_response) diff --git a/requirements.txt b/requirements.txt index cf56d5b..bd3a06f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests>=2 +httpx>=0.25.0