(2.0.0) woohoo, first major version bump!
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:
parent
81dea4c8a2
commit
cb87400278
17 changed files with 1489 additions and 36 deletions
|
@ -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 }}
|
||||
|
|
|
@ -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)
|
||||
|
|
29
docs/getting-started/installation.md
Normal file
29
docs/getting-started/installation.md
Normal 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
|
||||
```
|
0
docs/getting-started/usage.md
Normal file
0
docs/getting-started/usage.md
Normal file
BIN
docs/img/logo.png
Normal file
BIN
docs/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 526 KiB |
11
docs/index.md
Normal file
11
docs/index.md
Normal 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
3
docs/ref/exceptions.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Exceptions
|
||||
|
||||
::: pyflowery.exceptions
|
3
docs/ref/flowery.md
Normal file
3
docs/ref/flowery.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# FloweryAPI
|
||||
|
||||
::: pyflowery.pyflowery
|
3
docs/ref/models.md
Normal file
3
docs/ref/models.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Models Reference
|
||||
|
||||
::: pyflowery.models
|
9
docs/ref/rest_adapter.md
Normal file
9
docs/ref/rest_adapter.md
Normal 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
101
mkdocs.yml
Normal 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 © 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
1185
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -1 +1 @@
|
|||
VERSION = "1.0.6"
|
||||
VERSION = "2.0.0"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue