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/docs/.pages b/docs/.pages
index 0291060..8baa755 100644
--- a/docs/.pages
+++ b/docs/.pages
@@ -2,7 +2,8 @@ nav:
- index.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.
+
+
+
+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(),
+ )
+ ```
+
+
+
## 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_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..42d990f
--- /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...")
+
+ manual_client = AIHordeAPIManualClient()
+
+ image_generate_async_request = ImageGenerateAsyncRequest(
+ apikey="0000000000",
+ prompt="A cat in a hat",
+ models=["Deliberate"],
+ )
+
+ print("Submitting image generation request...")
+
+ response = manual_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 = manual_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 = manual_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..ed03465
--- /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 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()
diff --git a/examples/async_ai_horde_api_client_example.py b/examples/ai_horde_client/async_aihorde_manual_client_example.py
similarity index 89%
rename from examples/async_ai_horde_api_client_example.py
rename to examples/ai_horde_client/async_aihorde_manual_client_example.py
index a56ef50..71bc836 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()
+ manual_client = AIHordeAPIManualClient()
image_generate_async_request = ImageGenerateAsyncRequest(
apikey="0000000000",
@@ -21,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(),
)
@@ -41,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_,
)
@@ -62,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
new file mode 100644
index 0000000..5832d7b
--- /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 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/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..a5dda6c 100644
--- a/horde_sdk/ai_horde_api/apimodels/__init__.py
+++ b/horde_sdk/ai_horde_api/apimodels/__init__.py
@@ -1,11 +1,15 @@
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 (
DeleteImageGenerateRequest,
ImageGenerateStatusRequest,
ImageGenerateStatusResponse,
+ ImageGeneration,
)
from horde_sdk.ai_horde_api.apimodels.generate._submit import (
ImageGenerationJobSubmitRequest,
@@ -29,4 +33,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/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
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