(2.0.0) woohoo, first major version bump!
Some checks failed
Actions / build (push) Successful in 8s
Actions / lint (push) Failing after 13s
Actions / docs (push) Successful in 23s

bunch of stuff this update, including a full documentation site (still a WIP)
there is now a cache created whenever an instance of `FloweryAPI` is instantiated, so you don't have to query the api and iterate through the api response to retrieve a single voice anymore!
function names have also been changed with this update, hence the major version bump. `get_tts()`, `get_voices()`, and `get_voice()` have been renamed to `fetch_tts()`, `fetch_voices()`, and `fetch_voice()` respectively. `get_voices()` still exists, but with different functionality (that method retrieves voices from the internal cache instead of querying the flowery api)

!BREAKING
This commit is contained in:
Seaswimmer 2024-09-18 09:55:25 -04:00
parent 81dea4c8a2
commit cb87400278
Signed by: cswimr
GPG key ID: 3813315477F26F82
17 changed files with 1489 additions and 36 deletions

View file

@ -6,7 +6,7 @@ on:
jobs:
build:
runs-on: docker
container: www.coastalcommits.com/seaswimmerthefsh/actionscontainers-seacogs:latest
container: www.coastalcommits.com/cswimr/actions:pyflowery
steps:
- name: Checkout
uses: actions/checkout@v3
@ -39,13 +39,13 @@ jobs:
lint:
runs-on: docker
container: www.coastalcommits.com/seaswimmerthefsh/actionscontainers-seacogs:latest
container: www.coastalcommits.com/cswimr/actions:pyflowery
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install dependencies
run: poetry install
run: poetry install --with dev
- name: Analysing code with Ruff
run: poetry run ruff check $(git ls-files '*.py')
@ -53,3 +53,47 @@ jobs:
- name: Analysing code with Pylint
run: poetry run pylint --rcfile=.forgejo/workflows/config/.pylintrc $(git ls-files '*.py')
docs:
runs-on: docker
container: coastalcommits.com/cswimr/actions:pyflowery
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install dependencies
run: poetry install --with docs
- name: Set environment variables
uses: actions/env@v2
- name: Build documentation
run: |
export SITE_URL="https://$CI_ACTION_REF_NAME_SLUG.pyflowery.coastalcommits.com"
export EDIT_URI="src/branch/$CI_ACTION_REF_NAME/docs"
poetry run mkdocs build -v
- name: Deploy documentation
run: |
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
unset GITHUB_TOKEN
unset GITLAB_TOKEN
echo "${YELLOW}Deploying to ${BLUE}Meli ${YELLOW}on branch ${GREEN}$CI_ACTION_REF_NAME_SLUG${YELLOW}...\n"
npx -p "@getmeli/cli" meli upload ./site \
--url "https://pages.coastalcommits.com" \
--site "${{ vars.MELI_SITE_ID }}" \
--token "${{ secrets.MELI_SECRET }}" \
--release "$CI_ACTION_REF_NAME_SLUG/${{ env.GITHUB_SHA }}" \
--branch "$CI_ACTION_REF_NAME_SLUG"
echo "\n${YELLOW}Deployed to ${BLUE}Meli ${YELLOW}on branch ${GREEN}$CI_ACTION_REF_NAME_SLUG${YELLOW}!"
echo "${GREEN}https://$CI_ACTION_REF_NAME_SLUG.pyflowery.coastalcommits.com/"
env:
GITEA_TOKEN: ${{ secrets.COASTALCOMMITSTOKEN }}

View file

@ -1,7 +1,8 @@
# PyFlowery
[<img alt="Actions Status" src="https://www.coastalcommits.com/cswimr/PyFlowery/badges/workflows/actions.yaml/badge.svg">](https://www.coastalcommits.com/cswimr/PyFlowery/actions?workflow=actions.yaml)
[<img alt="PyPI - Version" src="https://img.shields.io/pypi/v/pyflowery">](https://pypi.org/project/pyflowery/)
[<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/pyflowery">](https://pypi.org/project/pyflowery/)
[<img alt="PyPI - License" src="https://img.shields.io/pypi/l/pyflowery">](https://www.coastalcommits.com/cswimr/pyflowery/src/branch/main/LICENSE/)
[<img alt="Actions Status" src="https://www.coastalcommits.com/cswimr/PyFlowery/badges/workflows/actions.yaml/badge.svg?style=plastic">](https://www.coastalcommits.com/cswimr/PyFlowery/actions?workflow=actions.yaml)
[<img alt="Documentation" src="https://img.shields.io/badge/docs-CoastalCommits%20Pages-3e83fd?logo=materialformkdocs&style=plastic">](https://pyflowery.coastalcommits.com)
[<img alt="PyPI - Version" src="https://img.shields.io/pypi/v/pyflowery?style=plastic">](https://pypi.org/project/pyflowery/)
[<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/pyflowery?style=plastic">](https://pypi.org/project/pyflowery/)
[<img alt="PyPI - License" src="https://img.shields.io/pypi/l/pyflowery?style=plastic">](https://www.coastalcommits.com/cswimr/pyflowery/src/branch/main/LICENSE/)
A simple async Python API wrapper for the [Flowery API](https://flowery.pw/about)

View file

@ -0,0 +1,29 @@
# Installation
This section will guide you through the installation process of PyFlowery.
## pip
You can use pip to install PyFlowery.
The command to use differs slightly depending on what operating system you use.
On Windows:
``` prolog title="Command Prompt"
py -m pip install pyflowery
```
On macOS and Linux:
``` prolog title="Bash"
python3 -m pip install pyflowery
```
## Poetry
You can also use [Poetry](https://python-poetry.org/) to store your dependencies.
Use the following command to install PyFlowery:
``` prolog title="Command Prompt / Shell"
poetry add pyflowery
```

View file

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

11
docs/index.md Normal file
View file

@ -0,0 +1,11 @@
# Welcome to PyFlowery
/// admonition | This project is in active development
type: warning
These docs are not complete yet, and there is a lot still to do.
///
**PyFlowery** is a Python API Wrapper for the [Flowery](https://flowery.pw/about) API.
Check out the [usage](getting-started/usage.md) section for further information.
Installation instructions can be found [here](getting-started/installation.md).

3
docs/ref/exceptions.md Normal file
View file

@ -0,0 +1,3 @@
# Exceptions
::: pyflowery.exceptions

3
docs/ref/flowery.md Normal file
View file

@ -0,0 +1,3 @@
# FloweryAPI
::: pyflowery.pyflowery

3
docs/ref/models.md Normal file
View file

@ -0,0 +1,3 @@
# Models Reference
::: pyflowery.models

9
docs/ref/rest_adapter.md Normal file
View file

@ -0,0 +1,9 @@
# Rest Adapter
/// admonition | Internal Functions
type: warning
These functions are meant for use in other parts of the module. You *probably* shouldn't be calling these manually.
If there's an endpoint method missing from the main [FloweryAPI](flowery.md) class, you should open an [issue](https://www.coastalcommits.com/cswimr/PyFlowery/issues) (or a [pull request](https://www.coastalcommits.com/cswimr/PyFlowery/pulls)).
///
::: pyflowery.rest_adapter

101
mkdocs.yml Normal file
View file

@ -0,0 +1,101 @@
site_name: PyFlowery Documentation
site_url: !ENV [SITE_URL, 'https://pyflowery.coastalcommits.com']
repo_name: CoastalCommits
repo_url: https://www.coastalcommits.com/cswimr/PyFlowery
edit_uri: !ENV [EDIT_URI, 'src/branch/main/docs']
copyright: Copyright &copy; 2024, cswimr
docs_dir: docs
site_author: cswimr
site_description: PyFlowery is an asynchronous Python library for interacting with the Flowery API.
nav:
- Home: index.md
- Getting Started:
- Installation: getting-started/installation.md
- Usage: getting-started/usage.md
- Reference:
- FloweryAPI: ref/flowery.md
- Exceptions: ref/exceptions.md
- Models: ref/models.md
- Rest Adapter: ref/rest_adapter.md
plugins:
- git-authors
- search
- social
- git-revision-date-localized:
enable_creation_date: true
type: timeago
- mkdocstrings:
default_handler: python
handlers:
python:
options:
docstring_options:
ignore_imit_summary: true
summary: true
show_root_toc_entry: false
filters:
- "!^_"
markdown_extensions:
- abbr
- attr_list
- md_in_html
- tables
- pymdownx.blocks.details
- pymdownx.blocks.admonition
- pymdownx.saneheaders
- pymdownx.magiclink
- pymdownx.mark
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
theme:
name: material
palette:
- media: '(prefers-color-scheme: light)'
scheme: default
primary: white
accent: light blue
toggle:
icon: material/toggle-switch
name: Switch to dark mode
- media: '(prefers-color-scheme: dark)'
scheme: slate
primary: black
accent: light blue
toggle:
icon: material/toggle-switch-off-outline
name: Switch to light mode
features:
- announce.dismiss
- content.code.annotate
- content.code.copy
- navigation.instant
- navigation.instant.progress
- navigation.tracking
- navigation.top
- navigation.sections
- navigation.indexes
- search.suggest
- search.highlight
- search.share
- toc.follow
logo: img/logo.png
favicon: img/logo.png
icon:
repo: simple/forgejo
watch:
- ./pyflowery

1185
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
from typing import AsyncGenerator
import asyncio
from typing import AsyncGenerator, List, Tuple
from pyflowery.models import FloweryAPIConfig, Language, Voice
from pyflowery.rest_adapter import RestAdapter
@ -14,22 +15,68 @@ class FloweryAPI:
def __init__(self, config: FloweryAPIConfig = FloweryAPIConfig()):
self.config = config
self.adapter = RestAdapter(config)
self._voices_cache: List[Voice] = []
asyncio.run(self._populate_voices_cache())
async def get_voice(self, voice_id: str) -> Voice:
"""Get a voice from the Flowery API
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.config.logger.info('Voices cache populated!')
def get_voices(self, voice_id: str | None = None, name: str | None = None) -> Tuple[Voice] | None:
"""Get a set of voices from the cache.
Args:
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.
"""
if voice_id:
voice = next((voice for voice in self._voices_cache if voice.id == voice_id))
return (voice,) or None
if name:
voices = []
for voice in self._voices_cache:
if voice.name == name:
voices.append(voice)
return tuple(voices) or None
raise ValueError('You must provide either a voice_id or a name!')
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.
Args:
voice_id (str): The ID of the voice
Raises:
ValueError: Raised when the voice is not found
TooManyRequests: Raised when the Flowery API returns a 429 status code
ClientError: Raised when the Flowery API returns a 4xx status code
InternalServerError: Raised when the Flowery API returns a 5xx status code
RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded
Returns:
Voice: The voice
"""
async for voice in self.get_voices():
async for voice in self.fetch_voices():
if voice.id == voice_id:
return voice
else:
raise ValueError(f'Voice with ID {voice_id} not found.')
async def get_voices(self) -> AsyncGenerator[Voice, None]:
"""Get a list of voices from the Flowery API
async def fetch_voices(self) -> AsyncGenerator[Voice, None]:
"""Fetch a list of voices from the Flowery API
Raises:
TooManyRequests: Raised when the Flowery API returns a 429 status code
ClientError: Raised when the Flowery API returns a 4xx status code
InternalServerError: Raised when the Flowery API returns a 5xx status code
RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded
Returns:
AsyncGenerator[Voice, None]: A generator of Voices
@ -44,8 +91,8 @@ class FloweryAPI:
language=Language(**voice['language']),
)
async def get_tts(self, text: str, voice: Voice | str | None = None, translate: bool = False, silence: int = 0, audio_format: str = 'mp3', speed: float = 1.0):
"""Get a TTS audio file from the Flowery API
async def fetch_tts(self, text: str, voice: Voice | str | None = None, translate: bool = False, silence: int = 0, audio_format: str = 'mp3', speed: float = 1.0):
"""Fetch a TTS audio file from the Flowery API
Args:
text (str): The text to convert to speech
@ -55,6 +102,13 @@ class FloweryAPI:
audio_format (str): The audio format to return
speed (float): The speed of the speech
Raises:
ValueError: Raised when the provided text is too long and `allow_truncation` in the `FloweryAPIConfig` class is set to `False` (default).
TooManyRequests: Raised when the Flowery API returns a 429 status code
ClientError: Raised when the Flowery API returns a 4xx status code
InternalServerError: Raised when the Flowery API returns a 5xx status code
RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded
Returns:
bytes: The audio file
"""

View file

@ -35,6 +35,12 @@ class RestAdapter:
params (dict): Python dictionary of query parameters to send with the request.
timeout (float): Number of seconds to wait for the request to complete.
Raises:
TooManyRequests: Raised when the Flowery API returns a 429 status code
ClientError: Raised when the Flowery API returns a 4xx status code
InternalServerError: Raised when the Flowery API returns a 5xx status code
RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded
Returns:
Result: A Result object containing the status code, message, and data from the request.
"""
@ -86,6 +92,12 @@ class RestAdapter:
params (dict): Python dictionary of query parameters to send with the request.
timeout (float): Number of seconds to wait for the request to complete.
Raises:
TooManyRequests: Raised when the Flowery API returns a 429 status code
ClientError: Raised when the Flowery API returns a 4xx status code
InternalServerError: Raised when the Flowery API returns a 5xx status code
RetryLimitExceeded: Raised when the retry limit defined in the `FloweryAPIConfig` class (default 3) is exceeded
Returns:
Result: A Result object containing the status code, message, and data from the request.
"""

View file

@ -1 +1 @@
VERSION = "1.0.6"
VERSION = "2.0.0"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "pyflowery"
version = "1.0.6"
version = "2.0.0"
description = "A Python API wrapper for the Flowery API"
authors = ["cswimr <seaswimmerthefsh@gmail.com>"]
readme = "README.md"
@ -15,10 +15,24 @@ classifiers = [
python = "^3.11"
aiohttp = "^3.9.5"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
pylint = "^3.2.7"
ruff = "^0.6.5"
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
mkdocs = "1.6.1"
mkdocstrings = {extras = ["python"], version = "0.26.1"}
mkdocs-git-authors-plugin = "0.9.0"
mkdocs-git-revision-date-localized-plugin = "1.2.9"
mkdocs-material = {extras = ["imaging"], version = "9.5.35"}
mkdocs-redirects = "1.2.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View file

@ -20,16 +20,10 @@ ALEXANDER = "fa3ea565-121f-5efd-b4e9-59895c77df23" # TikTok
JACOB = "38f45366-68e8-5d39-b1ef-3fd4eeb61cdb" # Microsoft Azure
STORMTROOPER = "191c5adc-a092-5eea-b4ff-ce01f66153ae" # TikTok
async def test_get_voices():
"""Test the get_voices method"""
async for voice in api.get_voices():
if 'en' in voice.language.code:
api.config.logger.info(voice)
async def test_get_tts():
"""Test the get_tts method"""
voice = await api.get_voice(voice_id=ALEXANDER)
tts = await api.get_tts(text="Sphinx of black quartz, judge my vow. The quick brown fox jumps over a lazy dog.", voice=voice)
async def test_fetch_tts():
"""Test the fetch_tts method"""
voice = api.get_voices(voice_id=ALEXANDER)[0]
tts = await api.fetch_tts(text="Sphinx of black quartz, judge my vow. The quick brown fox jumps over a lazy dog.", voice=voice)
try:
with open('test.mp3', 'wb') as f:
f.write(tts)
@ -37,12 +31,10 @@ async def test_get_tts():
api.config.logger.error(e, exc_info=True)
long_string = 'a' * 2049
try:
await api.get_tts(text=long_string)
await api.fetch_tts(text=long_string)
except ValueError as e:
api.config.logger.error("This is expected to fail:\n%s", e, exc_info=True)
if __name__ == '__main__':
api.config.logger.info("testing get_voices")
asyncio.run(test_get_voices())
api.config.logger.info("testing get_tts")
asyncio.run(test_get_tts())
api.config.logger.info("testing fetch_tts")
asyncio.run(test_fetch_tts())