From e14ecb2e3ee270a2c4a56ab7298cbfd09dc42f09 Mon Sep 17 00:00:00 2001 From: Aedial Date: Fri, 2 Jun 2023 05:00:18 +0200 Subject: [PATCH] [MISC] Overhaul docs --- docs/source/conf.py | 2 ++ .../novelai_api/novelai_api.high_level.rst | 7 ++++ .../novelai_api/novelai_api.low_level.rst | 7 ++++ docs/source/novelai_api/novelai_api.rst | 2 ++ example/boilerplate.py | 18 ++++++++++- novelai_api/NovelAI_API.py | 25 +++++++++++++++ novelai_api/_high_level.py | 15 ++++++++- novelai_api/_low_level.py | 14 ++++++++ novelai_api/utils.py | 32 +++++++++++++++++-- 9 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 docs/source/novelai_api/novelai_api.high_level.rst create mode 100644 docs/source/novelai_api/novelai_api.low_level.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index e314cf2..c82c62f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,6 +69,8 @@ "confval": "modal", # for custom object "mod": "modal", # for Python Sphinx Domain "class": "modal", # for Python Sphinx Domain + "attr": "tooltip", # for Python Sphinx Domain + "meth": "tooltip", # for Python Sphinx Domain } diff --git a/docs/source/novelai_api/novelai_api.high_level.rst b/docs/source/novelai_api/novelai_api.high_level.rst new file mode 100644 index 0000000..769f263 --- /dev/null +++ b/docs/source/novelai_api/novelai_api.high_level.rst @@ -0,0 +1,7 @@ +novelai\_api.high\_level +======================== + +.. automodule:: novelai_api._high_level + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/novelai_api/novelai_api.low_level.rst b/docs/source/novelai_api/novelai_api.low_level.rst new file mode 100644 index 0000000..3823ccb --- /dev/null +++ b/docs/source/novelai_api/novelai_api.low_level.rst @@ -0,0 +1,7 @@ +novelai\_api.low\_level +======================= + +.. automodule:: novelai_api._low_level + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/novelai_api/novelai_api.rst b/docs/source/novelai_api/novelai_api.rst index b10e05b..fe2b062 100644 --- a/docs/source/novelai_api/novelai_api.rst +++ b/docs/source/novelai_api/novelai_api.rst @@ -4,6 +4,8 @@ novelai-api package .. toctree:: :maxdepth: 2 + novelai_api.low_level + novelai_api.high_level novelai_api.BanList novelai_api.BiasGroup novelai_api.GlobalSettings diff --git a/example/boilerplate.py b/example/boilerplate.py index dfb01d7..e0fc6e9 100644 --- a/example/boilerplate.py +++ b/example/boilerplate.py @@ -12,7 +12,23 @@ class API: """ - Boilerplate for the redundant parts + Boilerplate for the redundant parts. + Using the object as a context manager will automatically login using the environment variables + ``NAI_USERNAME`` and ``NAI_PASSWORD``. + + Usage: + + .. code-block:: python + + async with API() as api: + api = api.api + encryption_key = api.encryption_key + logger = api.logger + ... # Do stuff + + + A custom base address can be passed to the constructor to replace the default + (:attr:`BASE_ADDRESS `) """ _username: str diff --git a/novelai_api/NovelAI_API.py b/novelai_api/NovelAI_API.py index 3e6f057..2a2b807 100644 --- a/novelai_api/NovelAI_API.py +++ b/novelai_api/NovelAI_API.py @@ -13,23 +13,48 @@ class NovelAIAPI: # Constants + + #: The base address for the API BASE_ADDRESS: str = "https://api.novelai.net" LIB_ROOT: str = dirname(abspath(__file__)) # Variables + + #: The logger for the API logger: Logger + #: The client session for the API (None if synchronous) session: Optional[ClientSession] + #: The timeout for a request (in seconds) timeout: ClientTimeout + #: The headers for a request headers: CIMultiDict + #: The cookies for a request cookies: SimpleCookie + #: The proxy for a request (None if no proxy) proxy: Optional[StrOrURL] = None + #: The proxy authentication for a request (None if no proxy) proxy_auth: Optional[BasicAuth] = None + # API parts + + #: The low-level API (thin wrapper) low_level: LowLevel + #: The high-level API (abstraction on top of low-level) high_level: HighLevel def __init__(self, session: Optional[ClientSession] = None, logger: Optional[Logger] = None): + """ + Create a new NovelAIAPI object, which can be used to interact with the API. + Use the low_level and high_level attributes for this purpose + + Use attach_session and detach_session to switch between synchronous and asynchronous requests + by attaching a ClientSession + + :param session: The ClientSession to use for requests (None for synchronous) + :param logger: The logger to use for the API (None for creating an empty default logger) + """ + # variable passing if session is not None and not isinstance(session, ClientSession): raise ValueError(f"Expected None or type 'ClientSession' for session, but got type '{type(session)}'") diff --git a/novelai_api/_high_level.py b/novelai_api/_high_level.py index 4a69640..45a0c9b 100644 --- a/novelai_api/_high_level.py +++ b/novelai_api/_high_level.py @@ -14,6 +14,17 @@ class HighLevel: + """ + High level API for NovelAI. This class is not meant to be used directly, + but rather through :attr:`NovelAIAPI.high_level `. + + The most relevant methods are: + + * :meth:`login ` + * :meth:`generate ` + * :meth:`generate_image ` + """ + _parent: "NovelAIAPI" # noqa: F821 def __init__(self, parent: "NovelAIAPI"): # noqa: F821 @@ -313,7 +324,9 @@ async def generate( **kwargs, ) -> Dict[str, Any]: """ - Generate text. The text is returned at once, when generation is finished. + Generate text. The b64-encoded text is returned at once, when generation is finished. + To decode the text, the :meth:`novelai_api.utils.b64_to_tokens` + and :meth:`novelai_api.Tokenizer.Tokenizer.decode` methods should be used. :param prompt: Context to give to the AI (raw text or list of tokens) :param model: Model to use for the AI diff --git a/novelai_api/_low_level.py b/novelai_api/_low_level.py index c4d5df3..8047745 100644 --- a/novelai_api/_low_level.py +++ b/novelai_api/_low_level.py @@ -47,9 +47,23 @@ def print_with_parameters(args: Dict[str, Any]): # === API === # class LowLevel: + """ + Low level API for NovelAI. This class is not meant to be used directly, + but rather through :attr:`NovelAIAPI.low_level `. + + The most relevant methods are: + + * :meth:`login ` + * :meth:`generate ` + * :meth:`generate_image ` + * :meth:`get_keystore ` + * :meth:`download_objects ` + """ + _parent: "NovelAIAPI" # noqa: F821 _is_async: bool + #: Enable or disable schema validation for responses. Default is ``True``. is_schema_validation_enabled: bool def __init__(self, parent: "NovelAIAPI"): # noqa: F821 diff --git a/novelai_api/utils.py b/novelai_api/utils.py index 906f436..26d7d8d 100644 --- a/novelai_api/utils.py +++ b/novelai_api/utils.py @@ -212,7 +212,7 @@ def decrypt_user_data(items: Union[List[Dict[str, Any]], Dict[str, Any]], keysto meta = item["meta"] if meta not in keystore: - raise NovelAIError(-1, f"Meta of item #{i} ({meta}) missing from keystore") + raise NovelAIError("", -1, f"Meta of item #{i} ({meta}) missing from keystore") key = keystore[meta] @@ -263,7 +263,7 @@ def encrypt_user_data(items: Union[List[Dict[str, Any]], Dict[str, Any]], keysto meta = item["meta"] if meta not in keystore: - raise NovelAIError(-1, f"Meta of item #{i} ({meta}) missing from keystore") + raise NovelAIError("", -1, f"Meta of item #{i} ({meta}) missing from keystore") key = keystore[meta] @@ -281,6 +281,10 @@ def link_content_to_story( stories: Dict[str, Union[str, int, Dict[str, Any]]], story_contents: Union[List[Dict[str, Any]], Dict[str, Any]], ): + """ + Link the story content to each story in :ref: stories + """ + if not isinstance(stories, (list, tuple)): stories = [stories] @@ -298,6 +302,10 @@ def link_content_to_story( def unlink_content_from_story(stories: Dict[str, Union[str, int, Dict[str, Any]]]): + """ + Remove the story content from each story in :ref: stories + """ + if not isinstance(stories, (list, tuple)): stories = [stories] @@ -307,20 +315,36 @@ def unlink_content_from_story(stories: Dict[str, Union[str, int, Dict[str, Any]] def get_decrypted_user_data(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + Filter out items that have not been decrypted + """ + return [item for item in items if item.get("decrypted", False)] def tokens_to_b64(tokens: Iterable[int]) -> str: + """ + Encode a list of tokens into a base64 string that can be sent to the API + """ + return b64encode(b"".join(t.to_bytes(2, "little") for t in tokens)).decode() def b64_to_tokens(b64: str) -> List[int]: + """ + Decode a base64 string returned by the API into a list of tokens + """ + b = b64decode(b64) return [int.from_bytes(b[i : i + 2], "little") for i in range(0, len(b), 2)] def extract_preset_data(presets: List[Dict[str, Any]]) -> Dict[str, Preset]: + """ + Transform a list of preset data into a dict of Preset objects indexed by their id + """ + preset_list = {} for preset_data in presets: decompress_user_data(preset_data) @@ -330,6 +354,10 @@ def extract_preset_data(presets: List[Dict[str, Any]]) -> Dict[str, Preset]: def tokenize_if_not(model: Model, o: Union[str, List[int]]) -> List[int]: + """ + Tokenize the string if it is not already tokenized + """ + if isinstance(o, list): return o