Skip to content

Commit

Permalink
feat: asyncio support, subpkg shuffle, remove builtin shadowing
Browse files Browse the repository at this point in the history
Additionally:
- adds a couple of examples to `examples/`
- renamed CancelImageGenerateRequest to DeleteImageGenerateRequest
  • Loading branch information
tazlin committed Jul 11, 2023
1 parent 727b531 commit 6954e5e
Show file tree
Hide file tree
Showing 25 changed files with 721 additions and 3,904 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

With the power of pydantic, you can simplify interfacing with the [AI-Horde's suite of APIs](https://github.com/db0/AI-Horde). Whether you want to request your own images, or roll your own worker software, this package may suit your needs for anything horde related.

## General notes
- Certain API models have attributes which may collide with a python builtin, such as `id` or `type`. In these cases, the attribute has a trailing underscore, as in `id_`. Ingested json still will work with the field 'id' (its a alias).

## AI-Horde
`TODO`

Expand Down
34 changes: 34 additions & 0 deletions examples/ai_horde_api_client.example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from horde_sdk.ai_horde_api import AIHordeAPIClient
from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest
from horde_sdk.generic_api import RequestErrorResponse


def do_generate_check(ai_horde_api_client: AIHordeAPIClient) -> None:
pass


def main() -> None:
"""Just a proof of concept - but several other pieces of functionality exist."""

ai_horde_api_client = AIHordeAPIClient()

image_generate_async_request = ImageGenerateAsyncRequest(
apikey="0000000000",
prompt="A cat in a hat",
models=["Deliberate"],
)

response = ai_horde_api_client.submit_request(
image_generate_async_request,
image_generate_async_request.get_success_response_type(),
)

if isinstance(response, RequestErrorResponse):
print(f"Error: {response.message}")
return

print(f"Job ID: {response.id_}")


if __name__ == "__main__":
main()
47 changes: 47 additions & 0 deletions examples/async_ai_horde_api_client_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

import asyncio

from horde_sdk.ai_horde_api import AIHordeAPIClient
from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest
from horde_sdk.generic_api import RequestErrorResponse


async def main() -> None:
print("Starting...")
ai_horde_api_client = AIHordeAPIClient()

image_generate_async_request = ImageGenerateAsyncRequest(
apikey="0000000000",
prompt="A cat in a hat",
models=["Deliberate"],
)

response = await ai_horde_api_client.async_submit_request(
image_generate_async_request,
image_generate_async_request.get_success_response_type(),
)

if isinstance(response, RequestErrorResponse):
print(f"Error: {response.message}")
return

# Keep making ImageGenerateCheckRequests until the job is done.
while True:
check_response = await ai_horde_api_client.async_get_generate_check(
apikey="0000000000",
generation_id=response.id_,
)

if isinstance(check_response, RequestErrorResponse):
print(f"Error: {check_response.message}")
return

if check_response.done:
break

await asyncio.sleep(5)


if __name__ == "__main__":
asyncio.run(main())
48 changes: 34 additions & 14 deletions example.py → examples/ratings_api_client_example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Short examples of how to use."""

import argparse
import os

import pydantic
from horde_sdk.ratings_api import (
Expand All @@ -12,25 +13,32 @@
UserValidateResponseRecord,
)

# See also in horde_sdk.ratings_api:
# UserCheckRequest,
# UserCheckResponse,
# UserRatingsRequest,
# UserRatingsResponse,


def main() -> None:
"""Just a proof of concept - but several other pieces of functionality exist."""
argParser = argparse.ArgumentParser()

argParser.add_argument("-k", "--key", required=True, help="Your horde API key.")
argParser.add_argument("-f", "--file", required=True, help="The file to write the response to.")
argParser.add_argument("-k", "--key", required=False, help="Your horde API key.")
argParser.add_argument("-o", "--output_file", required=True, help="The file to write the response to.")
argParser.add_argument("-u", "--user_id", required=True, help="The user_id (number only) to test against.")
args = argParser.parse_args()

# apiKey = os.environ.get("HORDE_API_KEY")
env_apikey = os.environ.get("HORDE_API_KEY")

# class args:
# key = CHANGE_ME
# user_id = "6572"
# file = "out.json"
if args.key is None and env_apikey is None:
print(
"You must provide an API key either via the -k/--key argument or the HORDE_API_KEY environment variable.",
)
exit(1)

ratingsAPIClient = RatingsAPIClient()
userValidateRequest = UserValidateRequest(
ratings_api_client = RatingsAPIClient()
user_validate_request = UserValidateRequest(
apikey=args.key,
user_id=args.user_id,
format=SelectableReturnFormats.json,
Expand All @@ -39,16 +47,28 @@ def main() -> None:
min_ratings=0,
)

response: pydantic.BaseModel = ratingsAPIClient.submit_request(
userValidateRequest,
userValidateRequest.get_success_response_type(),
print("Request URL:")
print(user_validate_request.get_endpoint_url())
print()
print("Request Body JSON:")
print(user_validate_request.model_dump_json())

response: pydantic.BaseModel = ratings_api_client.submit_request(
api_request=user_validate_request,
# Note that the type hint is accurate on the return type of this `get_success_response_type()`
expected_response_type=user_validate_request.get_success_response_type(),
)
if not isinstance(response, UserValidateResponse):
raise Exception("The response type doesn't match expected one!")

print("Response JSON:")
responseJson = response.model_dump_json()
print(responseJson)

print()
print("Response pydantic representation:")
print(responseJson)
print()
print("Select details from the response:")
print(f"{response.total=}")
first_rating: UserValidateResponseRecord = response.ratings[0]

Expand All @@ -58,7 +78,7 @@ def main() -> None:
print(f"{first_rating.average=}")
print(f"{first_rating.times_rated=}")

with open(args.file, "w") as fileOutHandle:
with open(args.output_file, "w") as fileOutHandle:
fileOutHandle.write(first_rating.model_dump_json())


Expand Down
25 changes: 25 additions & 0 deletions src/horde_sdk/ai_horde_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from horde_sdk.ai_horde_api.ai_horde_client import (
AIHordeAPIClient,
)
from horde_sdk.ai_horde_api.consts import (
ALCHEMY_FORMS,
GENERATION_STATE,
KNOWN_SAMPLERS,
KNOWN_SOURCE_PROCESSING,
WORKER_TYPE,
)
from horde_sdk.ai_horde_api.endpoints import (
AI_HORDE_BASE_URL,
AI_HORDE_API_URL_Literals,
)

__all__ = [
"AIHordeAPIClient",
"AI_HORDE_BASE_URL",
"AI_HORDE_API_URL_Literals",
"ALCHEMY_FORMS",
"GENERATION_STATE",
"KNOWN_SAMPLERS",
"KNOWN_SOURCE_PROCESSING",
"WORKER_TYPE",
]
120 changes: 101 additions & 19 deletions src/horde_sdk/ai_horde_api/ai_horde_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from loguru import logger

from horde_sdk.ai_horde_api.apimodels import (
CancelImageGenerateRequest,
ImageGenerateAsyncRequest,
ImageGenerateAsyncResponse,
DeleteImageGenerateRequest,
ImageGenerateCheckRequest,
ImageGenerateCheckResponse,
ImageGenerateStatusRequest,
ImageGenerateStatusResponse,
)
from horde_sdk.ai_horde_api.endpoints import AI_HORDE_BASE_URL
Expand Down Expand Up @@ -47,33 +48,100 @@ def _handle_api_error(self, error_response: RequestErrorResponse, endpoint_url:
logger.error(f"Endpoint: {endpoint_url}")
logger.error(f"Message: {error_response.message}")

def generate_image_async(
def get_generate_check(
self,
api_request: ImageGenerateAsyncRequest,
) -> ImageGenerateAsyncResponse | RequestErrorResponse:
"""Submit a request to the AI-Horde API to generate an image asynchronously.
apikey: str,
generation_id: GenerationID | str,
) -> ImageGenerateCheckResponse | RequestErrorResponse:
"""Check if a pending image request has finished generating from the AI-Horde API, and return
the status of it. Not to be confused with `get_generate_status` which returns the images too.
Args:
apikey (str): The API key to use for authentication.
generation_id (GenerationID | str): The ID of the request to check.
This is a call to the `/v2/generate/async` endpoint.
Returns:
ImageGenerateCheckResponse | RequestErrorResponse: The response from the API.
""" """"""
api_request = ImageGenerateCheckRequest(id=generation_id)

api_response = self.submit_request(api_request, api_request.get_success_response_type())
if isinstance(api_response, RequestErrorResponse):
self._handle_api_error(api_response, api_request.get_endpoint_url())

return api_response

async def async_get_generate_check(
self,
apikey: str,
generation_id: GenerationID | str,
) -> ImageGenerateCheckResponse | RequestErrorResponse:
"""Asynchronously check if a pending image request has finished generating from the AI-Horde API, and return
the status of it. Not to be confused with `get_generate_status` which returns the images too.
Args:
api_request (ImageGenerateAsyncRequest): The request to submit.
apikey (str): The API key to use for authentication.
generation_id (GenerationID | str): The ID of the request to check.
Returns:
ImageGenerateCheckResponse | RequestErrorResponse: The response from the API.
"""

api_request = ImageGenerateCheckRequest(id=generation_id)

api_response = await self.async_submit_request(api_request, api_request.get_success_response_type())
if isinstance(api_response, RequestErrorResponse):
self._handle_api_error(api_response, api_request.get_endpoint_url())

return api_response

def get_generate_status(
self,
apikey: str,
generation_id: GenerationID | str,
) -> ImageGenerateStatusResponse | RequestErrorResponse:
"""Get the status and any generated images for a pending image request from the AI-Horde API.
Raises:
RuntimeError: If the response type is not ImageGenerateAsyncResponse.
*Do not use this method more often than is necessary.* The AI-Horde API will rate limit you if you do.
Use `get_generate_check` instead to check the status of a pending image request.
Args:
apikey (str): The API key to use for authentication.
generation_id (GenerationID): The ID of the request to check.
Returns:
ImageGenerateAsyncResponse | RequestErrorResponse: The response from the API.
ImageGenerateStatusResponse | RequestErrorResponse: The response from the API.
"""
api_request = ImageGenerateStatusRequest(id=generation_id)

api_response = self.submit_request(api_request, api_request.get_success_response_type())
if isinstance(api_response, RequestErrorResponse):
self._handle_api_error(api_response, api_request.get_endpoint_url())
return api_response
if not isinstance(api_response, ImageGenerateAsyncResponse):
logger.error("Failed to generate an image asynchronously.")
logger.error(f"Unexpected response type: {type(api_response)}")
raise RuntimeError(
f"Unexpected response type. Expected ImageGenerateAsyncResponse, got {type(api_response)}",
)

return api_response

async def async_get_generate_status(
self,
apikey: str,
generation_id: GenerationID | str,
) -> ImageGenerateStatusResponse | RequestErrorResponse:
"""Asynchronously get the status and any generated images for a pending image request from the AI-Horde API.
*Do not use this method more often than is necessary.* The AI-Horde API will rate limit you if you do.
Use `get_generate_check` instead to check the status of a pending image request.
Args:
apikey (str): The API key to use for authentication.
generation_id (GenerationID): The ID of the request to check.
Returns:
ImageGenerateStatusResponse | RequestErrorResponse: The response from the API.
"""
api_request = ImageGenerateStatusRequest(id=generation_id)

api_response = await self.async_submit_request(api_request, api_request.get_success_response_type())
if isinstance(api_response, RequestErrorResponse):
self._handle_api_error(api_response, api_request.get_endpoint_url())
return api_response

return api_response

Expand All @@ -87,11 +155,25 @@ def delete_pending_image(
Args:
generation_id (GenerationID): The ID of the request to delete.
"""
api_request = CancelImageGenerateRequest(id=generation_id, apikey=apikey)
api_request = DeleteImageGenerateRequest(id=generation_id, apikey=apikey)

api_response = self.submit_request(api_request, api_request.get_success_response_type())
if isinstance(api_response, RequestErrorResponse):
self._handle_api_error(api_response, api_request.get_endpoint_url())
return api_response

return api_response

async def async_delete_pending_image(
self,
apikey: str,
generation_id: GenerationID | str,
) -> ImageGenerateStatusResponse | RequestErrorResponse:
api_request = DeleteImageGenerateRequest(id=generation_id, apikey=apikey)

api_response = await self.async_submit_request(api_request, api_request.get_success_response_type())
if isinstance(api_response, RequestErrorResponse):
self._handle_api_error(api_response, api_request.get_endpoint_url())
return api_response

return api_response
4 changes: 2 additions & 2 deletions src/horde_sdk/ai_horde_api/apimodels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from horde_sdk.ai_horde_api.apimodels.generate._check import ImageGenerateCheckRequest, ImageGenerateCheckResponse
from horde_sdk.ai_horde_api.apimodels.generate._pop import ImageGenerateJobPopRequest, ImageGenerateJobResponse
from horde_sdk.ai_horde_api.apimodels.generate._status import (
CancelImageGenerateRequest,
DeleteImageGenerateRequest,
ImageGenerateStatusRequest,
ImageGenerateStatusResponse,
)
Expand All @@ -22,7 +22,7 @@
"ImageGenerateJobResponse",
"ImageGenerateStatusRequest",
"ImageGenerateStatusResponse",
"CancelImageGenerateRequest",
"DeleteImageGenerateRequest",
"StatsImageModels",
"StatsModelsResponse",
"ImageGenerationJobSubmitRequest",
Expand Down
2 changes: 1 addition & 1 deletion src/horde_sdk/ai_horde_api/apimodels/_stats.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing_extensions import override

from horde_sdk.ai_horde_api.apimodels._base import BaseAIHordeRequest
from horde_sdk.ai_horde_api.apimodels.base import BaseAIHordeRequest
from horde_sdk.ai_horde_api.endpoints import AI_HORDE_API_URL_Literals
from horde_sdk.consts import HTTPMethod
from horde_sdk.generic_api.apimodels import BaseResponse
Expand Down
Loading

0 comments on commit 6954e5e

Please sign in to comment.