diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 55f722d..ba6c348 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1-alpha.1" + ".": "0.1.0-alpha.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index d87f59a..17a595b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 3 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beats-ai-beatsfoundation%2Fbeats-foundation-f711efcd99b00b3dc26c741770eecb8c17dc76bac1372c192aa232b4aae417e9.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beats-ai-beatsfoundation%2Fbeats-foundation-9e8f99d19503143b44d679b3743fc6791e0f6ece45048c58926ed10376d78af5.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index a82036a..bc3662c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.1.0-alpha.1 (2025-01-14) + +Full Changelog: [v0.0.1-alpha.1...v0.1.0-alpha.1](https://github.com/beatsfoundation/beats-foundation-sdk/compare/v0.0.1-alpha.1...v0.1.0-alpha.1) + +### Features + +* **api:** api update ([#8](https://github.com/beatsfoundation/beats-foundation-sdk/issues/8)) ([abe66f1](https://github.com/beatsfoundation/beats-foundation-sdk/commit/abe66f1a8d13dc74b713c81c26d824819b2ca0ac)) +* **api:** api update ([#9](https://github.com/beatsfoundation/beats-foundation-sdk/issues/9)) ([126444e](https://github.com/beatsfoundation/beats-foundation-sdk/commit/126444eb9473a7c1da054741c7f9aa658cb30dbd)) +* **api:** update via SDK Studio ([#6](https://github.com/beatsfoundation/beats-foundation-sdk/issues/6)) ([94dcca5](https://github.com/beatsfoundation/beats-foundation-sdk/commit/94dcca579b38b9c238093de3617f425934432f44)) + + +### Chores + +* remove custom code ([7cef31e](https://github.com/beatsfoundation/beats-foundation-sdk/commit/7cef31ea40ff84ec06b17bf92d487e9c011557ec)) +* update SDK settings ([#10](https://github.com/beatsfoundation/beats-foundation-sdk/issues/10)) ([8e3d6b5](https://github.com/beatsfoundation/beats-foundation-sdk/commit/8e3d6b51086c021cf77aee85ba8b79794dfa1ae0)) + ## 0.0.1-alpha.1 (2025-01-13) Full Changelog: [v0.0.1-alpha.0...v0.0.1-alpha.1](https://github.com/beatsfoundation/beats-foundation-sdk/compare/v0.0.1-alpha.0...v0.0.1-alpha.1) diff --git a/README.md b/README.md index b808f8e..7b94677 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# AI Creation Engine SDK, by the BEATS AI Foundation +# Beats Foundation Python API library -[![PyPI version](https://img.shields.io/pypi/v/beats_foundation.svg)](https://pypi.org/project/beats_foundation/) +[![PyPI version](https://img.shields.io/pypi/v/beats-foundation.svg)](https://pypi.org/project/beats-foundation/) -The AI Creation Engine SDK, by the BEATS AI Foundation, provides convenient access to the AI Creation Engine REST API from any Python 3.8+ +The Beats Foundation Python library provides convenient access to the Beats Foundation REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -10,13 +10,13 @@ It is generated with [Stainless](https://www.stainlessapi.com/). ## Documentation -The REST API documentation can be found on [docs.beats-foundation.com](https://docs.beats-foundation.com). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [docs.beatsfoundation.com](https://docs.beatsfoundation.com). The full API of this library can be found in [api.md](api.md). ## Installation ```sh # install from PyPI -pip install --pre beats_foundation +pip install --pre beats-foundation ``` ## Usage @@ -24,9 +24,14 @@ pip install --pre beats_foundation The full API of this library can be found in [api.md](api.md). ```python +import os from beats_foundation import BeatsFoundation -client = BeatsFoundation() +client = BeatsFoundation( + bearer_token=os.environ.get( + "BEATSFOUNDATION_BEARER_TOKEN" + ), # This is the default and can be omitted +) song = client.songs.retrieve( "REPLACE_ME", @@ -44,10 +49,15 @@ so that your Bearer Token is not stored in source control. Simply import `AsyncBeatsFoundation` instead of `BeatsFoundation` and use `await` with each API call: ```python +import os import asyncio from beats_foundation import AsyncBeatsFoundation -client = AsyncBeatsFoundation() +client = AsyncBeatsFoundation( + bearer_token=os.environ.get( + "BEATSFOUNDATION_BEARER_TOKEN" + ), # This is the default and can be omitted +) async def main() -> None: diff --git a/SECURITY.md b/SECURITY.md index 3cd52a3..b46a996 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,7 +20,7 @@ or products provided by Beats Foundation please follow the respective company's ### Beats Foundation Terms and Policies -Please contact dev-feedback@beats-foundation.com for any questions or concerns regarding security of our services. +Please contact dev-feedback@beatsfoundation.com for any questions or concerns regarding security of our services. --- diff --git a/api.md b/api.md index e029886..a7fdd9f 100644 --- a/api.md +++ b/api.md @@ -8,6 +8,6 @@ from beats_foundation.types import Song, SongCreateResponse, SongListResponse Methods: -- client.songs.create(\*\*params) -> object +- client.songs.create(\*\*params) -> SongCreateResponse - client.songs.retrieve(id) -> Song - client.songs.list(\*\*params) -> SongListResponse diff --git a/pyproject.toml b/pyproject.toml index de8b796..17c89f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] -name = "beats_foundation" -version = "0.0.1-alpha.1" +name = "beats-foundation" +version = "0.1.0-alpha.1" description = "The official Python library for the beats-foundation API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Beats Foundation", email = "dev-feedback@beats-foundation.com" }, +{ name = "Beats Foundation", email = "dev-feedback@beatsfoundation.com" }, ] dependencies = [ "httpx>=0.23.0, <1", diff --git a/src/beats_foundation/_client.py b/src/beats_foundation/_client.py index 20fb3c9..c26f3bb 100644 --- a/src/beats_foundation/_client.py +++ b/src/beats_foundation/_client.py @@ -91,7 +91,7 @@ def __init__( if base_url is None: base_url = os.environ.get("BEATS_FOUNDATION_BASE_URL") if base_url is None: - base_url = f"https://www.beatsfoundation.com/api" + base_url = f"https://www.beatsfoundation.com" super().__init__( version=__version__, @@ -259,7 +259,7 @@ def __init__( if base_url is None: base_url = os.environ.get("BEATS_FOUNDATION_BASE_URL") if base_url is None: - base_url = f"https://www.beatsfoundation.com/api" + base_url = f"https://www.beatsfoundation.com" super().__init__( version=__version__, diff --git a/src/beats_foundation/_version.py b/src/beats_foundation/_version.py index eec17e1..5477548 100644 --- a/src/beats_foundation/_version.py +++ b/src/beats_foundation/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "beats_foundation" -__version__ = "0.0.1-alpha.1" # x-release-please-version +__version__ = "0.1.0-alpha.1" # x-release-please-version diff --git a/src/beats_foundation/resources/songs.py b/src/beats_foundation/resources/songs.py index b21bd2e..c752b9a 100644 --- a/src/beats_foundation/resources/songs.py +++ b/src/beats_foundation/resources/songs.py @@ -21,6 +21,7 @@ from ..types.song import Song from .._base_client import make_request_options from ..types.song_list_response import SongListResponse +from ..types.song_create_response import SongCreateResponse __all__ = ["SongsResource", "AsyncSongsResource"] @@ -49,7 +50,6 @@ def create( self, *, prompt: str, - creator_wallet_address: str | NotGiven = NOT_GIVEN, genre: str | NotGiven = NOT_GIVEN, is_instrumental: bool | NotGiven = NOT_GIVEN, lyrics: str | NotGiven = NOT_GIVEN, @@ -60,22 +60,22 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> SongCreateResponse: """Generate a new AI song based on provided parameters. Rate limited to 2 calls per hour per API key. Args: - prompt: Text prompt for song generation - - creator_wallet_address: Wallet address of the creator + prompt: Text prompt for song generation (max 200 characters) genre: Musical genre - is_instrumental: Whether the song should be instrumental + is_instrumental: Whether the song should be instrumental. If the song is instrumental, the lyrics + will be ignored. - lyrics: Optional lyrics for the song + lyrics: Optional lyrics for the song. If not provided, the prompt will be used to + generate lyrics. mood: Mood of the song @@ -92,7 +92,6 @@ def create( body=maybe_transform( { "prompt": prompt, - "creator_wallet_address": creator_wallet_address, "genre": genre, "is_instrumental": is_instrumental, "lyrics": lyrics, @@ -103,7 +102,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SongCreateResponse, ) def retrieve( @@ -210,7 +209,6 @@ async def create( self, *, prompt: str, - creator_wallet_address: str | NotGiven = NOT_GIVEN, genre: str | NotGiven = NOT_GIVEN, is_instrumental: bool | NotGiven = NOT_GIVEN, lyrics: str | NotGiven = NOT_GIVEN, @@ -221,22 +219,22 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> SongCreateResponse: """Generate a new AI song based on provided parameters. Rate limited to 2 calls per hour per API key. Args: - prompt: Text prompt for song generation - - creator_wallet_address: Wallet address of the creator + prompt: Text prompt for song generation (max 200 characters) genre: Musical genre - is_instrumental: Whether the song should be instrumental + is_instrumental: Whether the song should be instrumental. If the song is instrumental, the lyrics + will be ignored. - lyrics: Optional lyrics for the song + lyrics: Optional lyrics for the song. If not provided, the prompt will be used to + generate lyrics. mood: Mood of the song @@ -253,7 +251,6 @@ async def create( body=await async_maybe_transform( { "prompt": prompt, - "creator_wallet_address": creator_wallet_address, "genre": genre, "is_instrumental": is_instrumental, "lyrics": lyrics, @@ -264,7 +261,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=SongCreateResponse, ) async def retrieve( diff --git a/src/beats_foundation/types/__init__.py b/src/beats_foundation/types/__init__.py index 415d320..16ed01c 100644 --- a/src/beats_foundation/types/__init__.py +++ b/src/beats_foundation/types/__init__.py @@ -6,3 +6,4 @@ from .song_list_params import SongListParams as SongListParams from .song_create_params import SongCreateParams as SongCreateParams from .song_list_response import SongListResponse as SongListResponse +from .song_create_response import SongCreateResponse as SongCreateResponse diff --git a/src/beats_foundation/types/song_create_params.py b/src/beats_foundation/types/song_create_params.py index 9e7e366..837cb1e 100644 --- a/src/beats_foundation/types/song_create_params.py +++ b/src/beats_foundation/types/song_create_params.py @@ -11,19 +11,22 @@ class SongCreateParams(TypedDict, total=False): prompt: Required[str] - """Text prompt for song generation""" - - creator_wallet_address: Annotated[str, PropertyInfo(alias="creatorWalletAddress")] - """Wallet address of the creator""" + """Text prompt for song generation (max 200 characters)""" genre: str """Musical genre""" is_instrumental: Annotated[bool, PropertyInfo(alias="isInstrumental")] - """Whether the song should be instrumental""" + """Whether the song should be instrumental. + + If the song is instrumental, the lyrics will be ignored. + """ lyrics: str - """Optional lyrics for the song""" + """Optional lyrics for the song. + + If not provided, the prompt will be used to generate lyrics. + """ mood: str """Mood of the song""" diff --git a/src/beats_foundation/types/song_create_response.py b/src/beats_foundation/types/song_create_response.py new file mode 100644 index 0000000..eb9363c --- /dev/null +++ b/src/beats_foundation/types/song_create_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .song import Song +from .._models import BaseModel + +__all__ = ["SongCreateResponse"] + + +class SongCreateResponse(BaseModel): + song: Optional[Song] = None diff --git a/tests/api_resources/test_songs.py b/tests/api_resources/test_songs.py index b9af42b..7602615 100644 --- a/tests/api_resources/test_songs.py +++ b/tests/api_resources/test_songs.py @@ -9,7 +9,7 @@ from tests.utils import assert_matches_type from beats_foundation import BeatsFoundation, AsyncBeatsFoundation -from beats_foundation.types import Song, SongListResponse +from beats_foundation.types import Song, SongListResponse, SongCreateResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,19 +22,18 @@ def test_method_create(self, client: BeatsFoundation) -> None: song = client.songs.create( prompt="prompt", ) - assert_matches_type(object, song, path=["response"]) + assert_matches_type(SongCreateResponse, song, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: BeatsFoundation) -> None: song = client.songs.create( prompt="prompt", - creator_wallet_address="creatorWalletAddress", genre="genre", is_instrumental=True, lyrics="lyrics", mood="mood", ) - assert_matches_type(object, song, path=["response"]) + assert_matches_type(SongCreateResponse, song, path=["response"]) @parametrize def test_raw_response_create(self, client: BeatsFoundation) -> None: @@ -45,7 +44,7 @@ def test_raw_response_create(self, client: BeatsFoundation) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" song = response.parse() - assert_matches_type(object, song, path=["response"]) + assert_matches_type(SongCreateResponse, song, path=["response"]) @parametrize def test_streaming_response_create(self, client: BeatsFoundation) -> None: @@ -56,7 +55,7 @@ def test_streaming_response_create(self, client: BeatsFoundation) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" song = response.parse() - assert_matches_type(object, song, path=["response"]) + assert_matches_type(SongCreateResponse, song, path=["response"]) assert cast(Any, response.is_closed) is True @@ -140,19 +139,18 @@ async def test_method_create(self, async_client: AsyncBeatsFoundation) -> None: song = await async_client.songs.create( prompt="prompt", ) - assert_matches_type(object, song, path=["response"]) + assert_matches_type(SongCreateResponse, song, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBeatsFoundation) -> None: song = await async_client.songs.create( prompt="prompt", - creator_wallet_address="creatorWalletAddress", genre="genre", is_instrumental=True, lyrics="lyrics", mood="mood", ) - assert_matches_type(object, song, path=["response"]) + assert_matches_type(SongCreateResponse, song, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncBeatsFoundation) -> None: @@ -163,7 +161,7 @@ async def test_raw_response_create(self, async_client: AsyncBeatsFoundation) -> assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" song = await response.parse() - assert_matches_type(object, song, path=["response"]) + assert_matches_type(SongCreateResponse, song, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncBeatsFoundation) -> None: @@ -174,7 +172,7 @@ async def test_streaming_response_create(self, async_client: AsyncBeatsFoundatio assert response.http_request.headers.get("X-Stainless-Lang") == "python" song = await response.parse() - assert_matches_type(object, song, path=["response"]) + assert_matches_type(SongCreateResponse, song, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 306946e..8fcee1d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -24,7 +24,12 @@ from beats_foundation._types import Omit from beats_foundation._models import BaseModel, FinalRequestOptions from beats_foundation._constants import RAW_RESPONSE_HEADER -from beats_foundation._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError +from beats_foundation._exceptions import ( + APIStatusError, + APITimeoutError, + BeatsFoundationError, + APIResponseValidationError, +) from beats_foundation._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, @@ -339,6 +344,16 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + def test_validate_headers(self) -> None: + client = BeatsFoundation(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {bearer_token}" + + with pytest.raises(BeatsFoundationError): + with update_env(**{"BEATSFOUNDATION_BEARER_TOKEN": Omit()}): + client2 = BeatsFoundation(base_url=base_url, bearer_token=None, _strict_response_validation=True) + _ = client2 + def test_default_query_option(self) -> None: client = BeatsFoundation( base_url=base_url, @@ -1113,6 +1128,16 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + def test_validate_headers(self) -> None: + client = AsyncBeatsFoundation(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {bearer_token}" + + with pytest.raises(BeatsFoundationError): + with update_env(**{"BEATSFOUNDATION_BEARER_TOKEN": Omit()}): + client2 = AsyncBeatsFoundation(base_url=base_url, bearer_token=None, _strict_response_validation=True) + _ = client2 + def test_default_query_option(self) -> None: client = AsyncBeatsFoundation( base_url=base_url,