From 367b095cfa2585a48f3f998c62d392caf4528c14 Mon Sep 17 00:00:00 2001 From: tazlin Date: Wed, 12 Jul 2023 22:41:11 -0400 Subject: [PATCH 1/3] docs: include installation guide page in nav --- docs/.pages | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/.pages b/docs/.pages index 0291060..b79f9d4 100644 --- a/docs/.pages +++ b/docs/.pages @@ -1,5 +1,6 @@ nav: - index.md + - installation.md - getting_started.md - faq.md - horde_sdk From 3d2218bfdf3dac4f8199950ebdf03880bf06214f Mon Sep 17 00:00:00 2001 From: tazlin Date: Thu, 13 Jul 2023 09:58:52 -0400 Subject: [PATCH 2/3] refactor: class renames, some clarification --- .env.template | 11 ++ CONTRIBUTING.md | 6 +- examples/ai_horde_api_client.example.py | 34 ---- .../aihorde_manual_client_example.py | 108 ++++++++++++ .../aihorde_simple_client_example.py | 22 +++ .../async_aihorde_manual_client_example.py} | 6 +- .../async_aihorde_simple_client_example.py | 47 ++++++ .../ratings_api_client_example.py | 0 horde_sdk/__init__.py | 18 ++ horde_sdk/ai_horde_api/__init__.py | 6 +- ...ai_horde_client.py => ai_horde_clients.py} | 156 ++++++++++++++++-- horde_sdk/ai_horde_api/apimodels/__init__.py | 2 + horde_sdk/ai_horde_api/endpoints.py | 8 +- .../{generic_client.py => generic_clients.py} | 14 +- horde_sdk/ratings_api/ratings_client.py | 4 +- requirements.txt | 1 + tests/ai_horde_api/test_ai_horde_api_calls.py | 34 +++- 17 files changed, 410 insertions(+), 67 deletions(-) create mode 100644 .env.template delete mode 100644 examples/ai_horde_api_client.example.py create mode 100644 examples/ai_horde_client/aihorde_manual_client_example.py create mode 100644 examples/ai_horde_client/aihorde_simple_client_example.py rename examples/{async_ai_horde_api_client_example.py => ai_horde_client/async_aihorde_manual_client_example.py} (95%) create mode 100644 examples/ai_horde_client/async_aihorde_simple_client_example.py rename examples/{ => ratings_client}/ratings_api_client_example.py (100%) rename horde_sdk/ai_horde_api/{ai_horde_client.py => ai_horde_clients.py} (61%) rename horde_sdk/generic_api/{generic_client.py => generic_clients.py} (98%) diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..ec749b1 --- /dev/null +++ b/.env.template @@ -0,0 +1,11 @@ +# Template +# Copy this file to .env and fill in the values +AI_HORDE_URL="https://aihorde.net/api/" +AI_HORDE_DEV_URL="http://localhost:7001/api/" + +AI_HORDE_DEV_APIKEY="devkey" + +RATINGS_URL="https://ratings.aihorde.net/api/" +RATINGS_DEV_URL="http://localhost:7002/api/" + +RATINGS_DEV_APIKEY=${AI_HORDE_DEV_APIKEY} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ceeec5..8c1bc78 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to horde_sdk -Here are a list of code quality tools this project uses: +## Code Quality Tools * [tox](https://tox.wiki/) - Creates virtual environments for CI or local pytest runs. @@ -19,3 +19,7 @@ Here are a list of code quality tools this project uses: - Static type safety - I recommending using the [mypy daemon](https://mypy.readthedocs.io/en/stable/mypy_daemon.html). - If you are using VSCode, I recommend the `matangover.mypy` extension, which implements this nicely. + +## Things to know + + * The `AI_HORDE_DEV_URL` environment variable overrides `AI_HORDE_URL`. This is useful for testing changes. diff --git a/examples/ai_horde_api_client.example.py b/examples/ai_horde_api_client.example.py deleted file mode 100644 index d01d5cc..0000000 --- a/examples/ai_horde_api_client.example.py +++ /dev/null @@ -1,34 +0,0 @@ -from horde_sdk.ai_horde_api import AIHordeAPISimpleClient -from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest -from horde_sdk.generic_api.apimodels import RequestErrorResponse - - -def do_generate_check(ai_horde_api_client: AIHordeAPISimpleClient) -> None: - pass - - -def main() -> None: - """Just a proof of concept - but several other pieces of functionality exist.""" - - ai_horde_api_client = AIHordeAPISimpleClient() - - 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() diff --git a/examples/ai_horde_client/aihorde_manual_client_example.py b/examples/ai_horde_client/aihorde_manual_client_example.py new file mode 100644 index 0000000..9e103ea --- /dev/null +++ b/examples/ai_horde_client/aihorde_manual_client_example.py @@ -0,0 +1,108 @@ +import time +from pathlib import Path + +import requests + +from horde_sdk.ai_horde_api import AIHordeAPIManualClient +from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest, ImageGenerateStatusRequest +from horde_sdk.generic_api.apimodels import RequestErrorResponse + + +def do_generate_check(ai_horde_api_client: AIHordeAPIManualClient) -> None: + pass + + +def main() -> None: + print("Starting...") + + ai_horde_api_client = AIHordeAPIManualClient() + + image_generate_async_request = ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ) + + print("Submitting image generation request...") + + 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("Image generation request submitted!") + image_done = False + start_time = time.time() + check_counter = 0 + # Keep making ImageGenerateCheckRequests until the job is done. + while not image_done: + if time.time() - start_time > 20 or check_counter == 0: + print(f"{time.time() - start_time} seconds elapsed ({check_counter} checks made)...") + start_time = time.time() + + check_counter += 1 + check_response = ai_horde_api_client.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: + print("Image is done!") + image_done = True + break + + time.sleep(5) + + # Get the image with a ImageGenerateStatusRequest. + image_generate_status_request = ImageGenerateStatusRequest( + id=response.id_, + ) + + status_response = ai_horde_api_client.submit_request( + image_generate_status_request, + image_generate_status_request.get_success_response_type(), + ) + + if isinstance(status_response, RequestErrorResponse): + print(f"Error: {status_response.message}") + return + + for image_gen in status_response.generations: + print("Image generation:") + print(f"ID: {image_gen.id}") + print(f"URL: {image_gen.img}") + # debug(image_gen) + print("Downloading image...") + + image_bytes = None + # image_gen.img is a url, download it using the requests library + try: + image_bytes = requests.get(image_gen.img).content + except Exception as e: + print(f"Error: {e}") + return + + if image_bytes is None: + print("Error: Could not download image.") + return + + # Open a file in write mode and write the image bytes to it. + dir_to_write_to = Path("examples/requested_images/") + dir_to_write_to.mkdir(parents=True, exist_ok=True) + filepath_to_write_to = dir_to_write_to / f"{image_gen.id}.webp" + with open(filepath_to_write_to, "wb") as image_file: + image_file.write(image_bytes) + + print(f"Image downloaded to {filepath_to_write_to}!") + + +if __name__ == "__main__": + main() diff --git a/examples/ai_horde_client/aihorde_simple_client_example.py b/examples/ai_horde_client/aihorde_simple_client_example.py new file mode 100644 index 0000000..59e5da1 --- /dev/null +++ b/examples/ai_horde_client/aihorde_simple_client_example.py @@ -0,0 +1,22 @@ +from horde_sdk.ai_horde_api.ai_horde_clients import AIHordeAPISimpleClient +from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest, ImageGeneration + + +def main() -> None: + simple_client = AIHordeAPISimpleClient() + + generations: list[ImageGeneration] = simple_client.image_generate_request( + ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ), + ) + + image = simple_client.generation_to_image(generations[0]) + + image.save("cat_in_hat.png") + + +if __name__ == "__main__": + main() diff --git a/examples/async_ai_horde_api_client_example.py b/examples/ai_horde_client/async_aihorde_manual_client_example.py similarity index 95% rename from examples/async_ai_horde_api_client_example.py rename to examples/ai_horde_client/async_aihorde_manual_client_example.py index a56ef50..d483559 100644 --- a/examples/async_ai_horde_api_client_example.py +++ b/examples/ai_horde_client/async_aihorde_manual_client_example.py @@ -1,19 +1,17 @@ -from __future__ import annotations - import asyncio import time from pathlib import Path import aiohttp -from horde_sdk.ai_horde_api import AIHordeAPISimpleClient +from horde_sdk.ai_horde_api import AIHordeAPIManualClient from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest, ImageGenerateStatusRequest from horde_sdk.generic_api.apimodels import RequestErrorResponse async def main() -> None: print("Starting...") - ai_horde_api_client = AIHordeAPISimpleClient() + ai_horde_api_client = AIHordeAPIManualClient() image_generate_async_request = ImageGenerateAsyncRequest( apikey="0000000000", diff --git a/examples/ai_horde_client/async_aihorde_simple_client_example.py b/examples/ai_horde_client/async_aihorde_simple_client_example.py new file mode 100644 index 0000000..18f8eee --- /dev/null +++ b/examples/ai_horde_client/async_aihorde_simple_client_example.py @@ -0,0 +1,47 @@ +import asyncio + +from horde_sdk.ai_horde_api.ai_horde_clients import AIHordeAPISimpleClient +from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest, ImageGeneration + + +async def main() -> None: + simple_client = AIHordeAPISimpleClient() + + generations: list[ImageGeneration] = await simple_client.async_image_generate_request( + ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ), + ) + + image = simple_client.generation_to_image(generations[0]) + image.save("cat_in_hat.png") + + # Do 2 requests at once. + multi_generations: tuple[list[ImageGeneration], list[ImageGeneration]] = await asyncio.gather( + simple_client.async_image_generate_request( + ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ), + ), + simple_client.async_image_generate_request( + ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ), + ), + ) + + multi_image_1 = simple_client.generation_to_image(multi_generations[0][0]) + multi_image_1.save("cat_in_hat_multi_1.png") + + multi_image_2 = simple_client.generation_to_image(multi_generations[1][0]) + multi_image_2.save("cat_in_hat_multi_2.png") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/ratings_api_client_example.py b/examples/ratings_client/ratings_api_client_example.py similarity index 100% rename from examples/ratings_api_client_example.py rename to examples/ratings_client/ratings_api_client_example.py diff --git a/horde_sdk/__init__.py b/horde_sdk/__init__.py index 7f35d4c..d72bea5 100644 --- a/horde_sdk/__init__.py +++ b/horde_sdk/__init__.py @@ -1 +1,19 @@ """Any model or helper useful for creating or interacting with a horde API.""" +import os + +import dotenv +from loguru import logger + +# If the current working directory contains a `.env` file, import the environment variables from it. +# This is useful for development. +dotenv.load_dotenv() + +if os.getenv("AI_HORDE_DEV_URL"): + logger.warning( + "AI_HORDE_DEV_URL is set.", + ) + +if os.getenv("RATINGS_DEV_URL"): + logger.warning( + "RATINGS_DEV_URL is set.", + ) diff --git a/horde_sdk/ai_horde_api/__init__.py b/horde_sdk/ai_horde_api/__init__.py index 99fc834..62c849b 100644 --- a/horde_sdk/ai_horde_api/__init__.py +++ b/horde_sdk/ai_horde_api/__init__.py @@ -1,5 +1,5 @@ -from horde_sdk.ai_horde_api.ai_horde_client import ( - AIHordeAPISimpleClient, +from horde_sdk.ai_horde_api.ai_horde_clients import ( + AIHordeAPIManualClient, ) from horde_sdk.ai_horde_api.consts import ( ALCHEMY_FORMS, @@ -14,7 +14,7 @@ ) __all__ = [ - "AIHordeAPISimpleClient", + "AIHordeAPIManualClient", "AI_HORDE_BASE_URL", "AI_HORDE_API_URL_Literals", "ALCHEMY_FORMS", diff --git a/horde_sdk/ai_horde_api/ai_horde_client.py b/horde_sdk/ai_horde_api/ai_horde_clients.py similarity index 61% rename from horde_sdk/ai_horde_api/ai_horde_client.py rename to horde_sdk/ai_horde_api/ai_horde_clients.py index 0e9879a..7e9b6a0 100644 --- a/horde_sdk/ai_horde_api/ai_horde_client.py +++ b/horde_sdk/ai_horde_api/ai_horde_clients.py @@ -1,30 +1,37 @@ """Definitions to help interact with the AI-Horde API.""" from __future__ import annotations +import asyncio +import base64 +import io +import time import urllib.parse +import aiohttp +import PIL.Image +import requests from loguru import logger from horde_sdk.ai_horde_api.apimodels import ( DeleteImageGenerateRequest, ImageGenerateAsyncRequest, - ImageGenerateAsyncResponse, ImageGenerateCheckRequest, ImageGenerateCheckResponse, ImageGenerateStatusRequest, ImageGenerateStatusResponse, + ImageGeneration, ) from horde_sdk.ai_horde_api.endpoints import AI_HORDE_BASE_URL from horde_sdk.ai_horde_api.fields import GenerationID from horde_sdk.ai_horde_api.metadata import AIHordePathData from horde_sdk.generic_api.apimodels import RequestErrorResponse -from horde_sdk.generic_api.generic_client import ( +from horde_sdk.generic_api.generic_clients import ( + GenericHordeAPIManualClient, GenericHordeAPISession, - GenericHordeAPISimpleClient, ) -class AIHordeAPISimpleClient(GenericHordeAPISimpleClient): +class AIHordeAPIManualClient(GenericHordeAPIManualClient): """Represent an API client specifically configured for the AI-Horde API.""" def __init__(self) -> None: @@ -187,11 +194,12 @@ async def async_delete_pending_image( return api_response -class AIHordeAPISession(AIHordeAPISimpleClient, GenericHordeAPISession): - """Represent an API session specifically configured for the AI-Horde API. +class AIHordeAPISession(AIHordeAPIManualClient, GenericHordeAPISession): + """Context handler representing an API session specifically configured for the AI-Horde API. If you make a request which requires follow up (such as a request to generate an image), this will delete the - generation in progress when the context manager exits. If you do not want this to happen, use `AIHordeAPIClient`. + generation in progress when the context manager exits. If you want to control this yourself, use + `AIHordeAPIManualClient` instead. """ def __enter__(self) -> AIHordeAPISession: @@ -209,10 +217,74 @@ async def __aenter__(self) -> AIHordeAPISession: return _self -class AIHordeAPIClient: - async def image_generate_request(self, image_gen_request: ImageGenerateAsyncRequest) -> ImageGenerateAsyncResponse: - async with AIHordeAPISession() as image_gen_client: - response = await image_gen_client.async_submit_request( +class AIHordeAPISimpleClient: + def generation_to_image(self, generation: ImageGeneration) -> PIL.Image.Image: + """Convert an image generation to a PIL image. + + Args: + generation (ImageGeneration): The image generation to convert. + + Returns: + PIL.Image.Image: The converted image. + + Raises: + ValueError: If the generation has no image, or the image could not be downloaded or parsed. + + """ + + if generation.img is None: + raise ValueError("Generation has no image") + + image_bytes: bytes | None = None + if urllib.parse.urlparse(generation.img).scheme in ["http", "https"]: + response = requests.get(generation.img) + if response.status_code != 200: + raise RuntimeError(f"Error downloading image: {response.status_code}") + + image_bytes = response.content + else: + image_bytes = base64.b64decode(generation.img) + + if image_bytes is None: + raise RuntimeError("Error downloading or parsing image") + + return PIL.Image.open(io.BytesIO(image_bytes)) + + async def async_generation_to_image(self, generation: ImageGeneration) -> PIL.Image.Image: + """Convert an image generation to a PIL image. + + Args: + generation (ImageGeneration): The image generation to convert. + + Returns: + PIL.Image.Image: The converted image. + + Raises: + ValueError: If the generation has no image, or the image could not be downloaded or parsed. + + """ + + if generation.img is None: + raise ValueError("Generation has no image") + + image_bytes: bytes | None = None + if urllib.parse.urlparse(generation.img).scheme in ["http", "https"]: + async with aiohttp.ClientSession() as session, session.get(generation.img) as response: + if response.status != 200: + raise RuntimeError(f"Error downloading image: {response.status}") + + image_bytes = await response.read() + else: + image_bytes = base64.b64decode(generation.img) + + if image_bytes is None: + raise RuntimeError("Error downloading or parsing image") + + return PIL.Image.open(io.BytesIO(image_bytes)) + + def image_generate_request(self, image_gen_request: ImageGenerateAsyncRequest) -> list[ImageGeneration]: + with AIHordeAPISession() as image_gen_client: + response = image_gen_client.submit_request( api_request=image_gen_request, expected_response_type=image_gen_request.get_success_response_type(), ) @@ -223,12 +295,70 @@ async def image_generate_request(self, image_gen_request: ImageGenerateAsyncRequ check_request_type = response.get_follow_up_default_request() follow_up_data = response.get_follow_up_data() check_request = check_request_type.model_validate(follow_up_data) - async with AIHordeAPISession() as check_client: + with AIHordeAPISession() as check_client: while True: - check_response = await check_client.async_submit_request( + check_response = check_client.submit_request( api_request=check_request, expected_response_type=check_request.get_success_response_type(), ) if isinstance(check_response, RequestErrorResponse): raise RuntimeError(f"Error response received: {check_response.message}") + + if check_response.done: + break + + time.sleep(5) + + status_request = ImageGenerateStatusRequest(id=response.id_) + with AIHordeAPISession() as status_client: + status_response = status_client.submit_request( + api_request=status_request, + expected_response_type=status_request.get_success_response_type(), + ) + + if isinstance(status_response, RequestErrorResponse): + raise RuntimeError(f"Error response received: {status_response.message}") + + return status_response.generations + + async def async_image_generate_request( + self, + image_gen_request: ImageGenerateAsyncRequest, + ) -> list[ImageGeneration]: + async with AIHordeAPISession() as ai_horde_session: + response = await ai_horde_session.async_submit_request( + api_request=image_gen_request, + expected_response_type=image_gen_request.get_success_response_type(), + ) + + if isinstance(response, RequestErrorResponse): + raise RuntimeError(f"Error response received: {response.message}") + + check_request_type = response.get_follow_up_default_request() + follow_up_data = response.get_follow_up_data() + check_request = check_request_type.model_validate(follow_up_data) + while True: + check_response = await ai_horde_session.async_submit_request( + api_request=check_request, + expected_response_type=check_request.get_success_response_type(), + ) + + if isinstance(check_response, RequestErrorResponse): + raise RuntimeError(f"Error response received: {check_response.message}") + + if check_response.done: + break + + await asyncio.sleep(5) + + status_request = ImageGenerateStatusRequest(id=response.id_) + status_response = await ai_horde_session.async_submit_request( + api_request=status_request, + expected_response_type=status_request.get_success_response_type(), + ) + + if isinstance(status_response, RequestErrorResponse): + raise RuntimeError(f"Error response received: {status_response.message}") + + return status_response.generations diff --git a/horde_sdk/ai_horde_api/apimodels/__init__.py b/horde_sdk/ai_horde_api/apimodels/__init__.py index 9776839..e300673 100644 --- a/horde_sdk/ai_horde_api/apimodels/__init__.py +++ b/horde_sdk/ai_horde_api/apimodels/__init__.py @@ -6,6 +6,7 @@ DeleteImageGenerateRequest, ImageGenerateStatusRequest, ImageGenerateStatusResponse, + ImageGeneration, ) from horde_sdk.ai_horde_api.apimodels.generate._submit import ( ImageGenerationJobSubmitRequest, @@ -29,4 +30,5 @@ "ImageGenerationJobSubmitResponse", "AllWorkersDetailsRequest", "AllWorkersDetailsResponse", + "ImageGeneration", ] diff --git a/horde_sdk/ai_horde_api/endpoints.py b/horde_sdk/ai_horde_api/endpoints.py index c44f47b..f66b01b 100644 --- a/horde_sdk/ai_horde_api/endpoints.py +++ b/horde_sdk/ai_horde_api/endpoints.py @@ -7,11 +7,11 @@ # TODO: Defer setting this? AI_HORDE_BASE_URL = "https://aihorde.net/api/" -if os.environ.get("HORDE_URL", None): - AI_HORDE_BASE_URL = os.environ["HORDE_URL"] +if os.environ.get("AI_HORDE_URL", None): + AI_HORDE_BASE_URL = os.environ["AI_HORDE_URL"] -if os.environ.get("HORDE_URL_DEBUG", None): - AI_HORDE_BASE_URL = os.environ["HORDE_URL_DEBUG"] +if os.environ.get("AI_HORDE_DEV_URL", None): + AI_HORDE_BASE_URL = os.environ["AI_HORDE_DEV_URL"] class AI_HORDE_API_URL_Literals(StrEnum): diff --git a/horde_sdk/generic_api/generic_client.py b/horde_sdk/generic_api/generic_clients.py similarity index 98% rename from horde_sdk/generic_api/generic_client.py rename to horde_sdk/generic_api/generic_clients.py index 8f56f2b..48c4b77 100644 --- a/horde_sdk/generic_api/generic_client.py +++ b/horde_sdk/generic_api/generic_clients.py @@ -44,7 +44,7 @@ class _ParsedRequest(BaseModel): """TypeVar for the response type.""" -class GenericHordeAPISimpleClient: +class GenericHordeAPIManualClient: """Interfaces with any flask API the horde provides, but provides little error handling. This is the no-frills, simple version of the client if you want to have more control over the request process. @@ -609,7 +609,7 @@ async def async_delete( ) -class GenericHordeAPISession(GenericHordeAPISimpleClient): +class GenericHordeAPISession(GenericHordeAPIManualClient): """A client which can perform arbitrary horde API requests, but also keeps track of requests' responses which need follow up. Use `submit_request` for synchronous requests, and `async_submit_request` for asynchronous requests. @@ -683,6 +683,11 @@ def __exit__(self, exc_type: type[Exception], exc_val: Exception, exc_tb: object self._handle_exit(request_to_follow_up, response_to_follow_up) def _handle_exit(self, request_to_follow_up: BaseRequest, response_to_follow_up: BaseResponse) -> None: + if isinstance(response_to_follow_up, RequestErrorResponse): + return + if not request_to_follow_up.is_recovery_enabled(): + return + recovery_request_type: type[BaseRequest] = request_to_follow_up.get_recovery_request_type() request_params: dict[str, object] = response_to_follow_up.get_follow_up_data() @@ -733,6 +738,11 @@ async def __aexit__(self, exc_type: type[Exception], exc_val: Exception, exc_tb: ) async def _handle_exit_async(self, request_to_follow_up: BaseRequest, response_to_follow_up: BaseResponse) -> None: + if isinstance(response_to_follow_up, RequestErrorResponse): + return + if not request_to_follow_up.is_recovery_enabled(): + return + recovery_request_type: type[BaseRequest] = request_to_follow_up.get_recovery_request_type() request_params: dict[str, object] = response_to_follow_up.get_follow_up_data() diff --git a/horde_sdk/ratings_api/ratings_client.py b/horde_sdk/ratings_api/ratings_client.py index 386f438..26318d2 100644 --- a/horde_sdk/ratings_api/ratings_client.py +++ b/horde_sdk/ratings_api/ratings_client.py @@ -1,9 +1,9 @@ """Definitions to help interact with the Ratings API.""" -from horde_sdk.generic_api.generic_client import GenericHordeAPISimpleClient +from horde_sdk.generic_api.generic_clients import GenericHordeAPIManualClient from horde_sdk.ratings_api.metadata import RatingsAPIPathFields, RatingsAPIQueryFields -class RatingsAPIClient(GenericHordeAPISimpleClient): +class RatingsAPIClient(GenericHordeAPIManualClient): """Represent a client specifically configured for the Ratings APi.""" def __init__(self) -> None: diff --git a/requirements.txt b/requirements.txt index 3f2f158..2bcc079 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ loguru aiohttp aiodns pillow +python-dotenv diff --git a/tests/ai_horde_api/test_ai_horde_api_calls.py b/tests/ai_horde_api/test_ai_horde_api_calls.py index 1486fed..0bd16b5 100644 --- a/tests/ai_horde_api/test_ai_horde_api_calls.py +++ b/tests/ai_horde_api/test_ai_horde_api_calls.py @@ -3,7 +3,7 @@ import pytest -from horde_sdk.ai_horde_api.ai_horde_client import AIHordeAPISession, AIHordeAPISimpleClient +from horde_sdk.ai_horde_api.ai_horde_clients import AIHordeAPIManualClient, AIHordeAPISession, AIHordeAPISimpleClient from horde_sdk.ai_horde_api.apimodels import ( AllWorkersDetailsRequest, AllWorkersDetailsResponse, @@ -11,6 +11,7 @@ ImageGenerateAsyncRequest, ImageGenerateAsyncResponse, ImageGenerateStatusResponse, + ImageGeneration, ) from horde_sdk.ai_horde_api.consts import WORKER_TYPE from horde_sdk.generic_api.apimodels import RequestErrorResponse @@ -29,10 +30,10 @@ def default_image_gen_request(self) -> ImageGenerateAsyncRequest: ) def test_AIHordeAPIClient_init(self) -> None: - AIHordeAPISimpleClient() + AIHordeAPIManualClient() def test_generate_async(self, default_image_gen_request: ImageGenerateAsyncRequest) -> None: - client = AIHordeAPISimpleClient() + client = AIHordeAPIManualClient() image_async_response: ImageGenerateAsyncResponse | RequestErrorResponse = client.submit_request( api_request=default_image_gen_request, @@ -60,7 +61,7 @@ def test_generate_async(self, default_image_gen_request: ImageGenerateAsyncReque assert isinstance(cancel_response, DeleteImageGenerateRequest.get_success_response_type()) def test_workers_all(self) -> None: - client = AIHordeAPISimpleClient() + client = AIHordeAPIManualClient() api_request = AllWorkersDetailsRequest(type=WORKER_TYPE.image) @@ -139,3 +140,28 @@ async def submit_request() -> None: # Run 5 concurrent requests using asyncio await asyncio.gather(*[asyncio.create_task(submit_request()) for _ in range(5)]) + + +def test_simple_client_image_generate(simple_image_gen_request: ImageGenerateAsyncRequest) -> None: + simple_client = AIHordeAPISimpleClient() + + generations: list[ImageGeneration] = simple_client.image_generate_request(simple_image_gen_request) + + assert len(generations) == 1 + + image = simple_client.generation_to_image(generations[0]) + + assert image is not None + + +@pytest.mark.asyncio +async def test_simple_client_async_image_generate(simple_image_gen_request: ImageGenerateAsyncRequest) -> None: + simple_client = AIHordeAPISimpleClient() + + generations: list[ImageGeneration] = await simple_client.async_image_generate_request(simple_image_gen_request) + + assert len(generations) == 1 + + image = await simple_client.async_generation_to_image(generations[0]) + + assert image is not None From 81cd5f5178d075cd0fce79e982589d8169aa6030 Mon Sep 17 00:00:00 2001 From: tazlin Date: Thu, 13 Jul 2023 11:12:00 -0400 Subject: [PATCH 3/3] docs: better `getting started`, style changes, `examples` --- docs/.pages | 4 +- docs/build_docs.py | 5 +- docs/examples.md | 83 +++++++++++++ docs/faq.md | 15 +-- docs/getting_started.md | 116 ++++++++++++++---- docs/horde_sdk/.pages | 2 +- .../horde_sdk/ai_horde_api/ai_horde_client.md | 2 - .../ai_horde_api/ai_horde_clients.md | 2 + docs/horde_sdk/generic_api/generic_client.md | 2 - docs/horde_sdk/generic_api/generic_clients.md | 2 + docs/installation.md | 9 -- docs/stylesheets/extra.css | 10 +- .../aihorde_manual_client_example.py | 8 +- .../aihorde_simple_client_example.py | 4 +- .../async_aihorde_manual_client_example.py | 8 +- .../async_aihorde_simple_client_example.py | 4 +- horde_sdk/ai_horde_api/apimodels/__init__.py | 5 +- mkdocs.yml | 9 ++ 18 files changed, 226 insertions(+), 64 deletions(-) create mode 100644 docs/examples.md delete mode 100644 docs/horde_sdk/ai_horde_api/ai_horde_client.md create mode 100644 docs/horde_sdk/ai_horde_api/ai_horde_clients.md delete mode 100644 docs/horde_sdk/generic_api/generic_client.md create mode 100644 docs/horde_sdk/generic_api/generic_clients.md delete mode 100644 docs/installation.md diff --git a/docs/.pages b/docs/.pages index b79f9d4..8baa755 100644 --- a/docs/.pages +++ b/docs/.pages @@ -1,9 +1,9 @@ nav: - index.md - - installation.md - getting_started.md - faq.md + - examples.md - horde_sdk - GitHub Repo: https://https://github.com/Haidra-Org/horde-sdk -order: asc +order: desc diff --git a/docs/build_docs.py b/docs/build_docs.py index e32ccd6..a3653d1 100644 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -29,7 +29,10 @@ def main() -> None: relative_folder.mkdir(parents=True, exist_ok=True) with open(relative_folder / ".pages", "w") as f: - f.write(f"title: {relative_folder.name}\n") + if relative_folder.name == "horde_sdk": + f.write("title: Horde SDK Code Reference") + else: + f.write(f"title: {relative_folder.name}") # Get all the files in the folder files_in_folder = list(folder.glob("*.py")) diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..ca886db --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,83 @@ +# Example Clients + +See `examples/` for a complete list. These examples are all made in mind with your current working directory as `horde_sdk` (e.g., `cd horde_sdk`). + +## Simple Client (sync) Example +From `examples/ai_horde_client/aihorde_simple_client_example.py`: + +``` python +from horde_sdk.ai_horde_api.ai_horde_clients import AIHordeAPISimpleClient +from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest, ImageGeneration + + +def simple_generate_example() -> None: + simple_client = AIHordeAPISimpleClient() + + generations: list[ImageGeneration] = simple_client.image_generate_request( + ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ), + ) + + image = simple_client.generation_to_image(generations[0]) + + image.save("cat_in_hat.png") + +if __name__ == "__main__": + simple_generate_example() +``` + +## Simple Client (using asyncio) Example +From `examples/ai_horde_client/async_aihorde_simple_client_example.py`: + +``` python + +import asyncio + +from horde_sdk.ai_horde_api.ai_horde_clients import AIHordeAPISimpleClient +from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest, ImageGeneration + + +async def async_simple_generate_example() -> None: + simple_client = AIHordeAPISimpleClient() + + generations: list[ImageGeneration] = await simple_client.async_image_generate_request( + ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ), + ) + + image = simple_client.generation_to_image(generations[0]) + image.save("cat_in_hat.png") + + # Do 2 requests at once. + multi_generations: tuple[list[ImageGeneration], list[ImageGeneration]] = await asyncio.gather( + simple_client.async_image_generate_request( + ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ), + ), + simple_client.async_image_generate_request( + ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + ), + ), + ) + + multi_image_1 = simple_client.generation_to_image(multi_generations[0][0]) + multi_image_1.save("cat_in_hat_multi_1.png") + + multi_image_2 = simple_client.generation_to_image(multi_generations[1][0]) + + multi_image_2.save("cat_in_hat_multi_2.png") +if __name__ == "__main__": + asyncio.run(async_simple_generate_example()) +``` diff --git a/docs/faq.md b/docs/faq.md index 4cde98e..4857110 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -4,9 +4,7 @@ title: Frequently Asked Questions > The objects returned by horde_sdk are immutable. If you need to change > something, you'll need to create a new object with the changes you -> want. See [pydantic's -> docs](https://docs.pydantic.dev/2.0/usage/validation_errors/#frozen_instance) -> for more information. +> want. See the [section in getting started](../getting_started/#faux-immutability-or-why-cant-i-change-this-attribute) for more info. # I don't like types. Why is this library so focused on them? @@ -14,14 +12,13 @@ title: Frequently Asked Questions > make it easier for other developers to understand what your code is > doing with [type > hints](https://docs.python.org/3/library/typing.html). These *hint* -> what data format class attributes or functions are expecting or will +> what data format variables or functions are expecting or will > return. This is how IDEs such as PyCharm or VSCode can provide > autocomplete and other helpful features. > -> If you don't like working with the objects within your own code, you -> can always translate between the types and dicts using pydantic's -> `.model_dump()` `.model_validate()`. There is also a convenience -> function -> `horde_sdk.generic_api.apimodels.HordeAPIModel.to_json_horde_sdk_safe` +> If you don't like working with the objects from this library within +> your own code, you can always translate between the types and dicts using +> pydantic's `.model_dump()` `.model_validate()`. There is also a convenience +> function [to_json_horde_sdk_safe()](../horde_sdk/generic_api/apimodels/#horde_sdk.generic_api.apimodels.HordeAPIModel.to_json_horde_sdk_safe) > which may be useful. All API models in this library have this method, > but certain other classes may not. diff --git a/docs/getting_started.md b/docs/getting_started.md index 6c64d57..67750a9 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -1,9 +1,100 @@ # Getting Started -See `installation` for installation instructions. +To get started, you need to install the package into your project: + +``` console +pip install horde_sdk +``` + +
+
+ Note +
+ This library requires python >3.10 +
+ +## First steps + +1. Choose a client for the API you wish to consume: + - For AI Horde, this is either: + - [AIHordeAPISimpleClient](../horde_sdk/ai_horde_api/ai_horde_clients/#horde_sdk.ai_horde_api.ai_horde_clients.AIHordeAPISimpleClient) (easier, more safeties) + + - [AIHordeAPIManualClient](../horde_sdk/ai_horde_api/ai_horde_clients/#horde_sdk.ai_horde_api.ai_horde_clients.AIHordeAPIManualClient) (more control, manual cleanup required) + +2. Find the `*Request` object type appropriate to what you want to do. (see also: [naming](../getting_started/#naming)) + - These objects types are always found in the `apimodels` namespace of the `*_api` sub package. + - e.g., [ImageGenerateAsyncRequest](../horde_sdk/ai_horde_api/apimodels/generate/_async/#horde_sdk.ai_horde_api.apimodels.generate._async.ImageGenerateAsyncRequest) + - **Note** that there is always one or more response types mapped to a request. You can get the default success response `type` like so: + + + ```python + >>> ImageGenerateAsyncRequest.get_success_response_type() + + + # Alternatively: + >>> image_gen_request = ImageGenerateAsyncRequest( ... ) # Removed for brevity + >>> image_gen_request.get_success_response_type() + + ``` + Accordingly, the [ImageGenerateAsyncResponse](../horde_sdk/ai_horde_api/apimodels/generate/_async/#horde_sdk.ai_horde_api.apimodels.generate._async.ImageGenerateAsyncResponse) type is expected to be the return type from the API. + +
+
+ Warning +
+ RequestErrorResponse may be also returned depending on the client you are using. Check the RequestErrorResponse.message attribute for info on the error encountered. +
+ +3. Construct the request as appropriate: +``` python +image_generate_async_request = ImageGenerateAsyncRequest( + apikey="0000000000", + prompt="A cat in a hat", + models=["Deliberate"], + params=ImageGenerationInputPayload( + width=512, + height=768, + sampler_name=KNOWN_SAMPLERS.k_euler_a, + clip_skip=1, + n=2, + ), +) +``` + +4. Submit the request: + + Simple Client: + ``` python + simple_client = AIHordeAPISimpleClient() + generations: list[ImageGeneration] = simple_client.image_generate_request( + image_generate_async_request, + ) + ``` + + Manual Client: + ``` python + manual_client = AIHordeAPIManualClient() + + response = manual_client.submit_request( + image_generate_async_request, + image_generate_async_request.get_success_response_type(), + ) + ``` +
+
+ Warning +
+ Manual clients may leave server resources tied up if you do not implement handling. See the important note about manual clients for more info. +
+ + ## General Notes and Guidance +### API Expectations +#### Important note about manual clients +A few endpoints, such as `/v2/generate/async` ([ImageGenerateAsyncRequest](../horde_sdk/ai_horde_api/apimodels/generate/_async/#horde_sdk.ai_horde_api.apimodels.generate._async.ImageGenerateAsyncRequest)), will have their operations live on the API server until they are retrieved or cancelled (in this case, with either a [ImageGenerateStatusRequest](../horde_sdk/ai_horde_api/apimodels/generate/_status/#horde_sdk.ai_horde_api.apimodels.generate._status.ImageGenerateStatusRequest) or [DeleteImageGenerateRequest](../horde_sdk/ai_horde_api/apimodels/generate/_status/#horde_sdk.ai_horde_api.apimodels.generate._status.DeleteImageGenerateRequest)). If you use a manual client, you are assuming responsibility for making a best-effort for cleaning up errant requests, especially if your implementation crashes. If you use a simple client, you do not have to worry about this, as [context handlers](../horde_sdk/generic_api/generic_clients/#horde_sdk.generic_api.generic_clients.GenericHordeAPISession) take care of this. + ### Typing - Under the hood, **this project is strongly typed**. Practically, @@ -31,15 +122,6 @@ See `installation` for installation instructions. or [model_validate_json](https://docs.pydantic.dev/2.0/api/main/#pydantic.main.BaseModel.model_validate_json) -### Inheritance - -- At first glance, the hierarchy of classes may seem a bit confusing. - Rest assured, however, that these very docs are very helpful here. - All relevant inherited attributes and functions are included in the - child class documentation. Accordingly if you are looking at the - code, and don't want to go up the hierarchy and figure out the - inheritance, use these docs to see the resolved attributes and - functions. ### Naming @@ -67,17 +149,3 @@ See `installation` for installation instructions. docs](https://docs.pydantic.dev/2.0/usage/validation_errors/#frozen_instance) for more information. See a - See also `faq` for more information. - -# Examples - -
- -
- -Note - -
- -TODO: Add examples - -
diff --git a/docs/horde_sdk/.pages b/docs/horde_sdk/.pages index d8b7cc5..ba063f3 100644 --- a/docs/horde_sdk/.pages +++ b/docs/horde_sdk/.pages @@ -1 +1 @@ -title: horde_sdk +title: Horde SDK Code Reference diff --git a/docs/horde_sdk/ai_horde_api/ai_horde_client.md b/docs/horde_sdk/ai_horde_api/ai_horde_client.md deleted file mode 100644 index 238094c..0000000 --- a/docs/horde_sdk/ai_horde_api/ai_horde_client.md +++ /dev/null @@ -1,2 +0,0 @@ -# ai_horde_client -::: horde_sdk.ai_horde_api.ai_horde_client diff --git a/docs/horde_sdk/ai_horde_api/ai_horde_clients.md b/docs/horde_sdk/ai_horde_api/ai_horde_clients.md new file mode 100644 index 0000000..51d8fe0 --- /dev/null +++ b/docs/horde_sdk/ai_horde_api/ai_horde_clients.md @@ -0,0 +1,2 @@ +# ai_horde_clients +::: horde_sdk.ai_horde_api.ai_horde_clients diff --git a/docs/horde_sdk/generic_api/generic_client.md b/docs/horde_sdk/generic_api/generic_client.md deleted file mode 100644 index 276785d..0000000 --- a/docs/horde_sdk/generic_api/generic_client.md +++ /dev/null @@ -1,2 +0,0 @@ -# generic_client -::: horde_sdk.generic_api.generic_client diff --git a/docs/horde_sdk/generic_api/generic_clients.md b/docs/horde_sdk/generic_api/generic_clients.md new file mode 100644 index 0000000..8c55647 --- /dev/null +++ b/docs/horde_sdk/generic_api/generic_clients.md @@ -0,0 +1,2 @@ +# generic_clients +::: horde_sdk.generic_api.generic_clients diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index 2ce5d57..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,9 +0,0 @@ -title: Installation - -To get started, you need to install the package into your project: - -``` console -pip install horde_sdk -``` - -Note that this package requires 3.10 or higher. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 4dc4c4f..7798e04 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -63,7 +63,15 @@ code { .md-nav__item--nested label { - font-style: italic; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 14px; + line-height: 1.5; + color: #333; + background-color: #f8f8f8; + border: 1px solid #ccc; + border-radius: 4px; + padding: 5px; + overflow-x: auto; } diff --git a/examples/ai_horde_client/aihorde_manual_client_example.py b/examples/ai_horde_client/aihorde_manual_client_example.py index 9e103ea..42d990f 100644 --- a/examples/ai_horde_client/aihorde_manual_client_example.py +++ b/examples/ai_horde_client/aihorde_manual_client_example.py @@ -15,7 +15,7 @@ def do_generate_check(ai_horde_api_client: AIHordeAPIManualClient) -> None: def main() -> None: print("Starting...") - ai_horde_api_client = AIHordeAPIManualClient() + manual_client = AIHordeAPIManualClient() image_generate_async_request = ImageGenerateAsyncRequest( apikey="0000000000", @@ -25,7 +25,7 @@ def main() -> None: print("Submitting image generation request...") - response = ai_horde_api_client.submit_request( + response = manual_client.submit_request( image_generate_async_request, image_generate_async_request.get_success_response_type(), ) @@ -45,7 +45,7 @@ def main() -> None: start_time = time.time() check_counter += 1 - check_response = ai_horde_api_client.get_generate_check( + check_response = manual_client.get_generate_check( apikey="0000000000", generation_id=response.id_, ) @@ -66,7 +66,7 @@ def main() -> None: id=response.id_, ) - status_response = ai_horde_api_client.submit_request( + status_response = manual_client.submit_request( image_generate_status_request, image_generate_status_request.get_success_response_type(), ) diff --git a/examples/ai_horde_client/aihorde_simple_client_example.py b/examples/ai_horde_client/aihorde_simple_client_example.py index 59e5da1..ed03465 100644 --- a/examples/ai_horde_client/aihorde_simple_client_example.py +++ b/examples/ai_horde_client/aihorde_simple_client_example.py @@ -2,7 +2,7 @@ from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest, ImageGeneration -def main() -> None: +def simple_generate_example() -> None: simple_client = AIHordeAPISimpleClient() generations: list[ImageGeneration] = simple_client.image_generate_request( @@ -19,4 +19,4 @@ def main() -> None: if __name__ == "__main__": - main() + simple_generate_example() diff --git a/examples/ai_horde_client/async_aihorde_manual_client_example.py b/examples/ai_horde_client/async_aihorde_manual_client_example.py index d483559..71bc836 100644 --- a/examples/ai_horde_client/async_aihorde_manual_client_example.py +++ b/examples/ai_horde_client/async_aihorde_manual_client_example.py @@ -11,7 +11,7 @@ async def main() -> None: print("Starting...") - ai_horde_api_client = AIHordeAPIManualClient() + manual_client = AIHordeAPIManualClient() image_generate_async_request = ImageGenerateAsyncRequest( apikey="0000000000", @@ -19,7 +19,7 @@ async def main() -> None: models=["Deliberate"], ) print("Submitting image generation request...") - response = await ai_horde_api_client.async_submit_request( + response = await manual_client.async_submit_request( image_generate_async_request, image_generate_async_request.get_success_response_type(), ) @@ -39,7 +39,7 @@ async def main() -> None: start_time = time.time() check_counter += 1 - check_response = await ai_horde_api_client.async_get_generate_check( + check_response = await manual_client.async_get_generate_check( apikey="0000000000", generation_id=response.id_, ) @@ -60,7 +60,7 @@ async def main() -> None: id=response.id_, ) - status_response = await ai_horde_api_client.async_submit_request( + status_response = await manual_client.async_submit_request( image_generate_status_request, image_generate_status_request.get_success_response_type(), ) diff --git a/examples/ai_horde_client/async_aihorde_simple_client_example.py b/examples/ai_horde_client/async_aihorde_simple_client_example.py index 18f8eee..5832d7b 100644 --- a/examples/ai_horde_client/async_aihorde_simple_client_example.py +++ b/examples/ai_horde_client/async_aihorde_simple_client_example.py @@ -4,7 +4,7 @@ from horde_sdk.ai_horde_api.apimodels import ImageGenerateAsyncRequest, ImageGeneration -async def main() -> None: +async def async_simple_generate_example() -> None: simple_client = AIHordeAPISimpleClient() generations: list[ImageGeneration] = await simple_client.async_image_generate_request( @@ -44,4 +44,4 @@ async def main() -> None: if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(async_simple_generate_example()) diff --git a/horde_sdk/ai_horde_api/apimodels/__init__.py b/horde_sdk/ai_horde_api/apimodels/__init__.py index e300673..a5dda6c 100644 --- a/horde_sdk/ai_horde_api/apimodels/__init__.py +++ b/horde_sdk/ai_horde_api/apimodels/__init__.py @@ -1,5 +1,8 @@ from horde_sdk.ai_horde_api.apimodels._stats import StatsImageModelsRequest, StatsModelsResponse -from horde_sdk.ai_horde_api.apimodels.generate._async import ImageGenerateAsyncRequest, ImageGenerateAsyncResponse +from horde_sdk.ai_horde_api.apimodels.generate._async import ( + ImageGenerateAsyncRequest, + ImageGenerateAsyncResponse, +) 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 ( diff --git a/mkdocs.yml b/mkdocs.yml index 5fb18f1..d58de5c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,6 +14,15 @@ plugins: separate_signature: true show_signature_annotations: true +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + theme: name: material