From 798b551138cc5b6503d36614d12dc499992b0e3f Mon Sep 17 00:00:00 2001 From: cswimr Date: Wed, 18 Sep 2024 13:49:36 -0400 Subject: [PATCH] (2.1.0) fix running inside of async loops and made user_agent mandatory this release finishes the usage.md page on the documentation, makes specifying a `user_agent` mandatory when instantiating a `FloweryAPIConfig` class, and fixes a bug that would prevent the `FloweryAPI` class from being instantiated inside of an async event loop --- docs/getting-started/usage.md | 96 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 + pyflowery/models.py | 12 ++--- pyflowery/pyflowery.py | 27 ++++++---- pyflowery/version.py | 2 +- pyproject.toml | 2 +- 6 files changed, 121 insertions(+), 20 deletions(-) diff --git a/docs/getting-started/usage.md b/docs/getting-started/usage.md index e69de29..f24a70e 100644 --- a/docs/getting-started/usage.md +++ b/docs/getting-started/usage.md @@ -0,0 +1,96 @@ +# Usage + +This page contains a few examples of how to use the `pyflowery` package. This page does not cover installation, for that see the [installation](installation.md) page. + +## Creating an API client + +To create an API client, you need to first import the `pyflowery.pyflowery.FloweryAPI` class. Then, you can create an instance of the class by passing in a `pyflowery.models.FloweryAPIConfig` class. + +```python +from pyflowery import FloweryAPI, FloweryAPIConfig +config = FloweryAPIConfig(user_agent="PyFlowery Documentation Example/example@gmail.com") +api = FloweryAPI(config) +``` + +Okay, now we have a `FloweryAPI` class. Let's move on to the next example. + +## Retrieving a voice + +So, whenever a `FloweryAPI` class is instantiated, it will automatically fetch a list of voices from the Flowery API, and cache it in the class. You can access this cache by calling the `get_voices` method with either a voice's ID or the name of a voice. If you want to get a list of all voices, you can call the `get_voices` method without any arguments. + +```python +# Set up the API client +from pyflowery import FloweryAPI, FloweryAPIConfig +config = FloweryAPIConfig(user_agent="PyFlowery Documentation Example/example@gmail.com") +api = FloweryAPI(config) # This will fetch all of the voices from the API and cache them automatically, you don't need to do that manually + +voices = api.get_voices(name="Alexander") +print(voices) # (Voice(id='fa3ea565-121f-5efd-b4e9-59895c77df23', name='Alexander', gender='Male', source='TikTok', language=Language(name='English (United States)', code='en-US')),) +print(voices[0].id) # 'fa3ea565-121f-5efd-b4e9-59895c77df23' +``` + +## Updating the API client's voice cache + +In most use cases, it is not necessary to manually update the voice cache. But, for applications that run for an extended period of time, it may be necessary to manually update the voice cache. To do this, you can call the `_populate_voices_cache()` async method. + +```python +import asyncio # This is required to run asynchronous code outside of async functions +from pyflowery import FloweryAPI, FloweryAPIConfig +config = FloweryAPIConfig(user_agent="PyFlowery Documentation Example/example@gmail.com") +api = FloweryAPI(config) # This will fetch all of the voices from the API and cache them automatically, you don't need to do that manually + +asyncio.run(api._populate_voices_cache()) # This will update the voice cache. This is what `FloweryAPI` calls automatically when it is instantiated +``` + +## Retrieving a list of voices from the API directly + +If necessary, you can call the `fetch_voices()` or `fetch_voice()` methods. These methods will fetch the voices from the API directly, skipping the cache. This isn't recommended, though, as it puts more strain on the Flowery API. + +=== "`fetch_voices()`" + `fetch_voices()` returns an `AsyncContextManager`, so you need to iterate through it when you call it. + + ```python + import asyncio + from pyflowery import FloweryAPI, FloweryAPIConfig + config = FloweryAPIConfig(user_agent="PyFlowery Documentation Example/example@gmail.com") + api = FloweryAPI(config) + + async def fetch_voices(): + voices_list = [] + async for voice in api.fetch_voices(): + voices_list.append(voice) + return voices_list + + voices = asyncio.run(fetch_voices()) + ``` + +=== "`fetch_voice()`" + + ```python + import asyncio + from pyflowery import FloweryAPI, FloweryAPIConfig + config = FloweryAPIConfig(user_agent="PyFlowery Documentation Example/example@gmail.com") + api = FloweryAPI(config) + + voice_id = "38f45366-68e8-5d39-b1ef-3fd4eeb61cdb" + + voice = asyncio.run(api.fetch_voice(voice_id)) + print(voice) # Voice(id='38f45366-68e8-5d39-b1ef-3fd4eeb61cdb', name='Jacob', gender='Male', source='Microsoft Azure', language=Language(name='English (United States)', code='en-US')) + ``` + +## Converting text to audio + +Finally, let's convert some text into audio. To do this, you can call the `fetch_tts()` method. This will return the bytes of the audio file. + +```python +import asyncio +from pyflowery import FloweryAPI, FloweryAPIConfig +config = FloweryAPIConfig(user_agent="PyFlowery Documentation Example/example@gmail.com") +api = FloweryAPI(config) + +voice = api.get_voices(name="Alexander")[0] + +tts = asyncio.run(api.fetch_tts("Hello, world!", voice)) +with open("hello_world.mp3", "wb") as f: + f.write(tts) +``` diff --git a/mkdocs.yml b/mkdocs.yml index 8b7728c..efdfa24 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,6 +52,8 @@ markdown_extensions: - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true - pymdownx.highlight: anchor_linenums: true line_spans: __span diff --git a/pyflowery/models.py b/pyflowery/models.py index e5a2960..ef43d4a 100644 --- a/pyflowery/models.py +++ b/pyflowery/models.py @@ -55,13 +55,13 @@ class FloweryAPIConfig: """Configuration for the Flowery API Attributes: - user_agent (str | None): User-Agent string to use for the HTTP requests + user_agent (str): User-Agent string to use for the HTTP requests. Required as of 2.1.0. logger (Logger): Logger to use for logging messages - allow_truncation (bool): Whether to allow truncation of text that is too long - retry_limit (int): Number of times to retry a request before giving up - interval (int): Seconds to wait between each retried request, multiplied by how many attempted requests have been made + allow_truncation (bool): Whether to allow truncation of text that is too long, defaults to `True` + retry_limit (int): Number of times to retry a request before giving up, defaults to `3` + interval (int): Seconds to wait between each retried request, multiplied by how many attempted requests have been made, defaults to `5` """ - user_agent: str | None = None + user_agent: str logger: Logger = getLogger('pyflowery') allow_truncation: bool = False retry_limit: int = 3 @@ -69,6 +69,4 @@ class FloweryAPIConfig: def prepended_user_agent(self) -> str: """Return the user_agent with the PyFlowery module version prepended""" - if not self.user_agent: - return f"PyFlowery/{VERSION} (Python {pyversion})" return f"PyFlowery/{VERSION} {self.user_agent} (Python {pyversion})" diff --git a/pyflowery/pyflowery.py b/pyflowery/pyflowery.py index e32e0f9..4b91c6e 100644 --- a/pyflowery/pyflowery.py +++ b/pyflowery/pyflowery.py @@ -1,5 +1,5 @@ import asyncio -from typing import AsyncGenerator, List, Tuple +from typing import AsyncGenerator, Tuple from pyflowery.models import FloweryAPIConfig, Language, Voice from pyflowery.rest_adapter import RestAdapter @@ -12,15 +12,23 @@ class FloweryAPI: config (FloweryAPIConfig): Configuration object for the API adapter (RestAdapter): Adapter for making HTTP requests """ - def __init__(self, config: FloweryAPIConfig = FloweryAPIConfig()): + def __init__(self, config: FloweryAPIConfig): self.config = config self.adapter = RestAdapter(config) - self._voices_cache: List[Voice] = [] - asyncio.run(self._populate_voices_cache()) + self._voices_cache: Tuple[Voice] = () + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + if loop and loop.is_running(): + self.config.logger.info("Async event loop is already running. Adding `_populate_voices_cache()` to the event loop.") + asyncio.create_task(self._populate_voices_cache()) + else: + asyncio.run(self._populate_voices_cache()) async def _populate_voices_cache(self): """Populate the voices cache. This method is called automatically when the FloweryAPI object is created, and should not be called directly.""" - self._voices_cache = [voice async for voice in self.fetch_voices()] + self._voices_cache = tuple([voice async for voice in self.fetch_voices()]) # pylint: disable=consider-using-generator self.config.logger.info('Voices cache populated!') def get_voices(self, voice_id: str | None = None, name: str | None = None) -> Tuple[Voice] | None: @@ -30,11 +38,8 @@ class FloweryAPI: voice_id (str): The ID of the voice name (str): The name of the voice - Raises: - ValueError: Raised when neither voice_id nor name is provided - Returns: - Tuple[Voice] | None: The voice or a list of voices, depending on the arguments provided and if there are multiple Voices with the same name. If no voice is found, returns None. + Tuple[Voice] | None: A tuple of Voice objects if found, otherwise None """ if voice_id: voice = next((voice for voice in self._voices_cache if voice.id == voice_id)) @@ -45,7 +50,7 @@ class FloweryAPI: if voice.name == name: voices.append(voice) return tuple(voices) or None - raise ValueError('You must provide either a voice_id or a name!') + return self._voices_cache or None async def fetch_voice(self, voice_id: str) -> Voice: """Fetch a voice from the Flowery API. This method bypasses the cache and directly queries the Flowery API. You should usually use `get_voice()` instead. @@ -66,7 +71,7 @@ class FloweryAPI: async for voice in self.fetch_voices(): if voice.id == voice_id: return voice - raise ValueError(f'Voice with ID {voice_id} not found.') + raise ValueError(f'Voice with ID {voice_id} not found.') async def fetch_voices(self) -> AsyncGenerator[Voice, None]: """Fetch a list of voices from the Flowery API diff --git a/pyflowery/version.py b/pyflowery/version.py index b46c2e7..127c148 100644 --- a/pyflowery/version.py +++ b/pyflowery/version.py @@ -1 +1 @@ -VERSION = "2.0.1" +VERSION = "2.1.0" diff --git a/pyproject.toml b/pyproject.toml index 4453fd7..f96ef1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyflowery" -version = "2.0.1" +version = "2.1.0" description = "A Python API wrapper for the Flowery API" authors = ["cswimr "] readme = "README.md"