diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index ef26a2d..c3a51b6 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ -# dataherald-python \ No newline at end of file +# Dataherald Python API library + +The Dataherald Python library provides convenient access to the Dataherald API from any Python application. The +library contains type definitions and easy access methods to synchronous and asynchronous methods. + +## Docmentation + +The API documentation can be found here. + +## Installation + +`pip install dataherald` + +## Usage + + The library needs to be configured with your accounts secret key which is available in the organization page of your + [Dataherald Dashboard](https://admin.dataherald.ai) + + ``` + import dataherald + dataherald.api_key = "dh-...." + + ``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d5f0e06 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "dataherald" +version = "0.0.2" +authors = [ + { name="Dataherald", email="support@dataherald.com" }, +] +description = "The official Python library for the Dataherald NL-to-SQL API" +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://github.com/dataherald/dataherald-python" +Issues = "https://github.com/dataherald/dataherald-python/issues" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = [ + "src/*" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/dataherald"] + +[tool.black] +line-length = 120 +target-version = ["py37"] \ No newline at end of file diff --git a/src/dataherald/__init__.py b/src/dataherald/__init__.py new file mode 100644 index 0000000..8c5214f --- /dev/null +++ b/src/dataherald/__init__.py @@ -0,0 +1,10 @@ +from ._client import ( + Client, + Dataherald, + AsyncDataherald, + Timeout, +) + +from ._exceptions import( + DataheraldError +) \ No newline at end of file diff --git a/src/dataherald/_base_client.py b/src/dataherald/_base_client.py new file mode 100644 index 0000000..381815d --- /dev/null +++ b/src/dataherald/_base_client.py @@ -0,0 +1,24 @@ +from typing import ( + Generic, + TypeVar, + Union +) + +_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) + + +class BaseClient(Generic[_HttpxClientT]): + +class SyncAPIClient(BaseClient[httpx.Client]): + _client: httpx.Client + + def __init__(): + super().__init__() + + + +class AsyncAPIClient(BaseClient[httpx.AsyncClient]): + _client: httpx.AsyncClient + + def __init__(): + super().__init__() \ No newline at end of file diff --git a/src/dataherald/_client.py b/src/dataherald/_client.py new file mode 100644 index 0000000..f99ef8b --- /dev/null +++ b/src/dataherald/_client.py @@ -0,0 +1,59 @@ +import httpx + +import os +from typing import Union + +from . import resources, _exceptions +from ._base_client import DEFAULT_MAX_RETRIES, SyncAPIClient, AsyncAPIClient +from ._types import ( + Timout, + NotGiven +) + +from _exceptions import DataheraldError + +class Dataherald(SyncAPIClient): + sql_generations: resources.SQLGenerations + nl_generations: resources.NLGenerations + organization: str | None + + + def __init__( + self, + timeout: Union[float, None], + api_key: str = None, + base_url:str | httpx.URL | None = None, + organization: str | None = None, + max_retries: int = DEFAULT_MAX_RETRIES, + ) -> None: + """ + Constructs a new synchronous Dataherald client instance + + This will infer the following arguements from the environment variables if they are not provided + - `api_key` - `DATAHERALD_API_KEY` + - `organization` - `DATAHERALD_ORG_ID` + """ + if api_key is None: + api_key = os.environ.get("DATAHERALD_API_KEY") + if api_key is None: + raise DataheraldError( + "No API key provided. Please provide an API key or set the DATAHERALD_API_KEY environment variable." + ) + self.api_key = api_key + + if organization is None: + organization = os.environ.get("OPENAI_ORG_ID") + self.organization = organization + + if base_url is None: + base_url = os.environ.get("OPENAI_BASE_URL") + if base_url is None: + base_url = f"https://api.dataherald.ai/api/v1" + + + +class AsyncDataherald(AsyncAPIClient): + pass + + + diff --git a/src/dataherald/_constants.py b/src/dataherald/_constants.py new file mode 100644 index 0000000..fd0dd28 --- /dev/null +++ b/src/dataherald/_constants.py @@ -0,0 +1,7 @@ +import httpx + + +# default timeout is 10 minutes +DEFAULT_TIMEOUT = httpx.Timeout(timeout=600.0, connect=5.0) +DEFAULT_MAX_RETRIES = 2 +DEFAULT_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) \ No newline at end of file diff --git a/src/dataherald/_exceptions.py b/src/dataherald/_exceptions.py new file mode 100644 index 0000000..44fcbde --- /dev/null +++ b/src/dataherald/_exceptions.py @@ -0,0 +1,48 @@ +import httpx +from typing import Any, Optional, cast +from typing_extensions import TypeGuard + + + +#To-Do move these to a utils class +def is_dict(obj: object) -> TypeGuard[dict[object, object]]: + return isinstance(obj, dict) + + +class DataheraldError(Exception): + pass + + +class APIError(DataheraldError): + message: str + request: httpx.Request + + body: object | None + """The API response body. + + If the API responded with a valid JSON structure then this property will be the + decoded result. + + If it isn't a valid JSON structure then this will be the raw response. + + If there was no response associated with this error then it will be `None`. + """ + + code: Optional[str] + param: Optional[str] + type: Optional[str] + + def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: + super().__init__(message) + self.request = request + self.message = message + self.body = body + + if is_dict(body): + self.code = cast(Any, body.get("code")) + self.param = cast(Any, body.get("param")) + self.type = cast(Any, body.get("type")) + else: + self.code = None + self.param = None + self.type = None \ No newline at end of file diff --git a/src/dataherald/_resource.py b/src/dataherald/_resource.py new file mode 100644 index 0000000..41e63cf --- /dev/null +++ b/src/dataherald/_resource.py @@ -0,0 +1,35 @@ +import asyncio +from typing import TYPE_CHECKING +import time + + +class SyncAPIResource: + _client: Dataherald + + def __init__(self, client: Dataherald) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + def _sleep(self, seconds: float) -> None: + time.sleep(seconds) + + +class AsyncAPIResource: + _client: AsyncDataherald + + def __init__(self, client: AsyncDataherald) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + async def _sleep(self, seconds: float) -> None: + await asyncio.sleep(seconds) \ No newline at end of file diff --git a/src/dataherald/resources/__init__.py b/src/dataherald/resources/__init__.py new file mode 100644 index 0000000..007ce58 --- /dev/null +++ b/src/dataherald/resources/__init__.py @@ -0,0 +1,9 @@ +from .sql_generations import SQLGenerations +from .nl_generation import NLGeneration +from .heartbeat import Heartbeat + +__all__ = [ + "SQLGenerations", + "NLGeneration", + "Heartbeat", +] \ No newline at end of file diff --git a/src/dataherald/resources/heartbeat.py b/src/dataherald/resources/heartbeat.py new file mode 100644 index 0000000..1b1a71a --- /dev/null +++ b/src/dataherald/resources/heartbeat.py @@ -0,0 +1,8 @@ +import httpx +from.._resource import SyncAPIResource +from .._client import Dataherald + +class HeartBeat(SyncAPIResource): + def __init__(self, client: Dataherald): + super.__init__(client) + return self.response(200, "OK") \ No newline at end of file diff --git a/src/dataherald/resources/nl_generations.py b/src/dataherald/resources/nl_generations.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dataherald/resources/sql_generations.py b/src/dataherald/resources/sql_generations.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/heartbeat.py b/tests/heartbeat.py new file mode 100644 index 0000000..302faaa --- /dev/null +++ b/tests/heartbeat.py @@ -0,0 +1,5 @@ +from ..src.dataherald._client import Dataherald, AsyncDataherald + + +if __name__ == "__main__": + print('yello') \ No newline at end of file