refactor(clients): reorganize client modules into clients package

- Move auth, user, and music clients into new clients package
- Update imports to use new module paths
- Add __init__.py to expose clients from package
- Clean up code formatting and imports
- Add pre-commit configuration for code quality checks
This commit is contained in:
2025-06-07 06:57:01 -07:00
parent eac722e37f
commit 161a4d42d6
14 changed files with 191 additions and 128 deletions

2
.gitignore vendored
View File

@ -170,4 +170,4 @@ cython_debug/
venv/* venv/*
Documents.md Documents.md
test.py test.py

35
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,35 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-added-large-files
- id: check-ast
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: detect-private-key
- id: file-contents-sorter
- id: mixed-line-ending
- id: name-tests-test
- id: pretty-format-json
args: [--autofix]
- id: debug-statements
- id: check-docstring-first
- id: requirements-txt-fixer
- repo: https://github.com/psf/black
rev: 24.2.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/PyCQA/isort
rev: 6.0.1
hooks:
- id: isort
args: ["--profile", "black"]

View File

@ -204,18 +204,18 @@ try:
# Step 1: Initiate login # Step 1: Initiate login
verify_id = client.auth.login("960", "7777777") verify_id = client.auth.login("960", "7777777")
print(f"OTP sent! Verification ID: {verify_id}") print(f"OTP sent! Verification ID: {verify_id}")
# Step 2: Get OTP from user # Step 2: Get OTP from user
otp = input("Enter OTP: ") otp = input("Enter OTP: ")
# Step 3: Verify OTP and get token # Step 3: Verify OTP and get token
token = client.auth.verify_otp(otp, verify_id) token = client.auth.verify_otp(otp, verify_id)
print(f"Login successful! Token: {token}") print(f"Login successful! Token: {token}")
# Step 4: Use the API # Step 4: Use the API
user_info = client.user.get_me() user_info = client.user.get_me()
print(f"Welcome, {user_info['username']}!") print(f"Welcome, {user_info['username']}!")
except AuthenticationError as e: except AuthenticationError as e:
print(f"Authentication failed: {e}") print(f"Authentication failed: {e}")
finally: finally:
@ -232,7 +232,7 @@ for category in results:
print(f"\n{category['title']}:") print(f"\n{category['title']}:")
for item in category['items'][:3]: # Show top 3 for item in category['items'][:3]: # Show top 3
print(f" {item['heading']} - {item['sub_heading']}") print(f" {item['heading']} - {item['sub_heading']}")
# Get detailed info if it's a song # Get detailed info if it's a song
if category['title'] == 'Songs': if category['title'] == 'Songs':
try: try:
@ -281,4 +281,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
--- ---
**Note**: This is an unofficial client library. songbox is a trademark of its respective owners. **Note**: This is an unofficial client library. songbox is a trademark of its respective owners.

View File

@ -1,2 +1,2 @@
boto3==1.38.32
httpx==0.28.1 httpx==0.28.1
boto3==1.38.32

View File

@ -1,6 +1,6 @@
"""Setup script for songbox API Client Library""" """Setup script for songbox API Client Library"""
from setuptools import setup, find_packages from setuptools import find_packages, setup
with open("README.md", "r", encoding="utf-8") as fh: with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read() long_description = fh.read()
@ -8,10 +8,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
# with open("songbox/requirements.txt", "r", encoding="utf-8") as fh: # with open("songbox/requirements.txt", "r", encoding="utf-8") as fh:
# requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")] # requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")]
requirements = [ requirements = ["httpx==0.28.1", "boto3==1.38.32"]
"httpx==0.28.1",
"boto3==1.38.32"
]
setup( setup(
name="songbox", name="songbox",
@ -59,4 +56,4 @@ setup(
"Source": "https://git.cubable.date/CustomIcon/songbox", "Source": "https://git.cubable.date/CustomIcon/songbox",
"Documentation": "https://git.cubable.date/CustomIcon/songbox/src/branch/master/README.md", "Documentation": "https://git.cubable.date/CustomIcon/songbox/src/branch/master/README.md",
}, },
) )

View File

@ -4,15 +4,13 @@ A Python client library for the songbox music streaming API.
""" """
from .client import SongboxClient from .client import SongboxClient
from .auth import AuthClient from .clients import AuthClient, MusicClient, UserClient
from .user import UserClient
from .music import MusicClient
from .exceptions import ( from .exceptions import (
songboxError,
AuthenticationError,
APIError, APIError,
AuthenticationError,
NotFoundError,
RateLimitError, RateLimitError,
NotFoundError songboxError,
) )
__version__ = "1.0.0" __version__ = "1.0.0"
@ -29,4 +27,4 @@ __all__ = [
"APIError", "APIError",
"RateLimitError", "RateLimitError",
"NotFoundError", "NotFoundError",
] ]

View File

@ -3,9 +3,9 @@
import argparse import argparse
import json import json
import sys import sys
from typing import Optional
from .client import SongboxClient from .client import SongboxClient
from .exceptions import songboxError, AuthenticationError from .exceptions import AuthenticationError, songboxError
def create_parser() -> argparse.ArgumentParser: def create_parser() -> argparse.ArgumentParser:
@ -20,98 +20,110 @@ Examples:
songbox auth login --country-code 960 --mobile 7777777 songbox auth login --country-code 960 --mobile 7777777
songbox auth verify --otp 123456 --verify-id <verify_id> songbox auth verify --otp 123456 --verify-id <verify_id>
songbox user me songbox user me
# Search for music # Search for music
songbox music search "huttaa" songbox music search "huttaa"
# Get album information # Get album information
songbox music album 808 songbox music album 808
# Download a song # Download a song
songbox music download "https://example.com/song.mp3" songbox music download "https://example.com/song.mp3"
songbox music download "https://example.com/song.mp3" --path ./my_music songbox music download "https://example.com/song.mp3" --path ./my_music
# Get user's following list # Get user's following list
songbox user following songbox user following
""" """,
) )
parser.add_argument( parser.add_argument(
"--token", "--token",
help="Authentication token (can also be set via songbox_TOKEN env var)" help="Authentication token (can also be set via songbox_TOKEN env var)",
) )
parser.add_argument( parser.add_argument(
"--base-url", "--base-url",
default="https://lavafoshi.online", default="https://lavafoshi.online",
help="Base URL for the API (default: https://lavafoshi.online)" help="Base URL for the API (default: https://lavafoshi.online)",
) )
parser.add_argument( parser.add_argument(
"--format", "--format",
choices=["json", "table", "simple"], choices=["json", "table", "simple"],
default="simple", default="simple",
help="Output format (default: simple)" help="Output format (default: simple)",
) )
subparsers = parser.add_subparsers(dest="command", help="Available commands") subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Auth commands # Auth commands
auth_parser = subparsers.add_parser("auth", help="Authentication commands") auth_parser = subparsers.add_parser("auth", help="Authentication commands")
auth_subparsers = auth_parser.add_subparsers(dest="auth_command") auth_subparsers = auth_parser.add_subparsers(dest="auth_command")
# Login command # Login command
login_parser = auth_subparsers.add_parser("login", help="Initiate login with mobile number") login_parser = auth_subparsers.add_parser(
login_parser.add_argument("--country-code", required=True, help="Country code (e.g., 960)") "login", help="Initiate login with mobile number"
)
login_parser.add_argument(
"--country-code", required=True, help="Country code (e.g., 960)"
)
login_parser.add_argument("--mobile", required=True, help="Mobile number") login_parser.add_argument("--mobile", required=True, help="Mobile number")
# Verify OTP command # Verify OTP command
verify_parser = auth_subparsers.add_parser("verify", help="Verify OTP and get token") verify_parser = auth_subparsers.add_parser(
"verify", help="Verify OTP and get token"
)
verify_parser.add_argument("--otp", required=True, help="OTP code") verify_parser.add_argument("--otp", required=True, help="OTP code")
verify_parser.add_argument("--verify-id", required=True, help="Verification ID from login") verify_parser.add_argument(
"--verify-id", required=True, help="Verification ID from login"
)
# Refresh token command # Refresh token command
auth_subparsers.add_parser("refresh", help="Refresh authentication token") auth_subparsers.add_parser("refresh", help="Refresh authentication token")
# Logout command # Logout command
auth_subparsers.add_parser("logout", help="Logout (clear token)") auth_subparsers.add_parser("logout", help="Logout (clear token)")
# User commands # User commands
user_parser = subparsers.add_parser("user", help="User commands") user_parser = subparsers.add_parser("user", help="User commands")
user_subparsers = user_parser.add_subparsers(dest="user_command") user_subparsers = user_parser.add_subparsers(dest="user_command")
user_subparsers.add_parser("me", help="Get current user information") user_subparsers.add_parser("me", help="Get current user information")
user_subparsers.add_parser("following", help="Get following list") user_subparsers.add_parser("following", help="Get following list")
# Music commands # Music commands
music_parser = subparsers.add_parser("music", help="Music commands") music_parser = subparsers.add_parser("music", help="Music commands")
music_subparsers = music_parser.add_subparsers(dest="music_command") music_subparsers = music_parser.add_subparsers(dest="music_command")
# Search command # Search command
search_parser = music_subparsers.add_parser("search", help="Search for music") search_parser = music_subparsers.add_parser("search", help="Search for music")
search_parser.add_argument("query", help="Search query") search_parser.add_argument("query", help="Search query")
# Album command # Album command
album_parser = music_subparsers.add_parser("album", help="Get album information") album_parser = music_subparsers.add_parser("album", help="Get album information")
album_parser.add_argument("album_id", help="Album ID") album_parser.add_argument("album_id", help="Album ID")
# Song command # Song command
song_parser = music_subparsers.add_parser("song", help="Get song information") song_parser = music_subparsers.add_parser("song", help="Get song information")
song_parser.add_argument("song_id", help="Song ID") song_parser.add_argument("song_id", help="Song ID")
# Artist command # Artist command
artist_parser = music_subparsers.add_parser("artist", help="Get artist information") artist_parser = music_subparsers.add_parser("artist", help="Get artist information")
artist_parser.add_argument("artist_id", help="Artist ID") artist_parser.add_argument("artist_id", help="Artist ID")
# Trending command # Trending command
trending_parser = music_subparsers.add_parser("trending", help="Get trending music") trending_parser = music_subparsers.add_parser("trending", help="Get trending music")
trending_parser.add_argument("--limit", type=int, help="Limit number of results") trending_parser.add_argument("--limit", type=int, help="Limit number of results")
# Download command # Download command
download_parser = music_subparsers.add_parser("download", help="Download a song") download_parser = music_subparsers.add_parser("download", help="Download a song")
download_parser.add_argument("song_url", help="Song URL to download (e.g., from song['url_original'])") download_parser.add_argument(
download_parser.add_argument("--path", help="Download directory path (default: ./downloads)") "song_url", help="Song URL to download (e.g., from song['url_original'])"
)
download_parser.add_argument(
"--path", help="Download directory path (default: ./downloads)"
)
return parser return parser
@ -159,21 +171,25 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
try: try:
verify_id = client.auth.login(args.country_code, args.mobile) verify_id = client.auth.login(args.country_code, args.mobile)
print(f"OTP sent! Verification ID: {verify_id}") print(f"OTP sent! Verification ID: {verify_id}")
print(f"Use this command to verify: songbox auth verify --otp <OTP> --verify-id {verify_id}") print(
f"Use this command to verify: songbox auth verify --otp <OTP> --verify-id {verify_id}"
)
except Exception as e: except Exception as e:
print(f"Login failed: {e}", file=sys.stderr) print(f"Login failed: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif args.auth_command == "verify": elif args.auth_command == "verify":
try: try:
token = client.auth.verify_otp(args.otp, args.verify_id) token = client.auth.verify_otp(args.otp, args.verify_id)
print(f"Authentication successful!") print(f"Authentication successful!")
print(f"Token: {token}") print(f"Token: {token}")
print(f"Save this token and use it with --token flag or songbox_TOKEN env var") print(
f"Save this token and use it with --token flag or songbox_TOKEN env var"
)
except Exception as e: except Exception as e:
print(f"Verification failed: {e}", file=sys.stderr) print(f"Verification failed: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif args.auth_command == "refresh": elif args.auth_command == "refresh":
try: try:
new_token = client.auth.refresh_token() new_token = client.auth.refresh_token()
@ -182,7 +198,7 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
except Exception as e: except Exception as e:
print(f"Token refresh failed: {e}", file=sys.stderr) print(f"Token refresh failed: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif args.auth_command == "logout": elif args.auth_command == "logout":
client.auth.logout() client.auth.logout()
print("Logged out successfully!") print("Logged out successfully!")
@ -197,7 +213,7 @@ def handle_user_commands(client: SongboxClient, args) -> None:
except Exception as e: except Exception as e:
print(f"Failed to get user info: {e}", file=sys.stderr) print(f"Failed to get user info: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif args.user_command == "following": elif args.user_command == "following":
try: try:
following = client.user.get_following() following = client.user.get_following()
@ -222,7 +238,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
except Exception as e: except Exception as e:
print(f"Search failed: {e}", file=sys.stderr) print(f"Search failed: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif args.music_command == "album": elif args.music_command == "album":
try: try:
album = client.music.get_album(args.album_id) album = client.music.get_album(args.album_id)
@ -230,7 +246,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
except Exception as e: except Exception as e:
print(f"Failed to get album: {e}", file=sys.stderr) print(f"Failed to get album: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif args.music_command == "song": elif args.music_command == "song":
try: try:
song = client.music.get_song(args.song_id) song = client.music.get_song(args.song_id)
@ -238,7 +254,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
except Exception as e: except Exception as e:
print(f"Failed to get song: {e}", file=sys.stderr) print(f"Failed to get song: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif args.music_command == "artist": elif args.music_command == "artist":
try: try:
artist = client.music.get_artist(args.artist_id) artist = client.music.get_artist(args.artist_id)
@ -246,7 +262,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
except Exception as e: except Exception as e:
print(f"Failed to get artist: {e}", file=sys.stderr) print(f"Failed to get artist: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif args.music_command == "download": elif args.music_command == "download":
try: try:
file_path = client.music.download_song(args.song_url, args.path) file_path = client.music.download_song(args.song_url, args.path)
@ -257,42 +273,48 @@ def handle_music_commands(client: SongboxClient, args) -> None:
sys.exit(1) sys.exit(1)
def main(): def main():
"""Main CLI entry point.""" """Main CLI entry point."""
parser = create_parser() parser = create_parser()
args = parser.parse_args() args = parser.parse_args()
if not args.command: if not args.command:
parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
# Get token from args or environment # Get token from args or environment
import os import os
token = args.token or os.getenv("songbox_TOKEN") token = args.token or os.getenv("songbox_TOKEN")
# Create client # Create client
try: try:
client = SongboxClient(base_url=args.base_url) client = SongboxClient(base_url=args.base_url)
# Set token if provided # Set token if provided
if token: if token:
client.set_token(token) client.set_token(token)
# Handle commands # Handle commands
if args.command == "auth": if args.command == "auth":
handle_auth_commands(client, args) handle_auth_commands(client, args)
elif args.command == "user": elif args.command == "user":
if not client.is_authenticated: if not client.is_authenticated:
print("Authentication required. Please login first or provide a token.", file=sys.stderr) print(
"Authentication required. Please login first or provide a token.",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
handle_user_commands(client, args) handle_user_commands(client, args)
elif args.command == "music": elif args.command == "music":
if not client.is_authenticated: if not client.is_authenticated:
print("Authentication required. Please login first or provide a token.", file=sys.stderr) print(
"Authentication required. Please login first or provide a token.",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
handle_music_commands(client, args) handle_music_commands(client, args)
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nOperation cancelled.", file=sys.stderr) print("\nOperation cancelled.", file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -300,9 +322,9 @@ def main():
print(f"Error: {e}", file=sys.stderr) print(f"Error: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
finally: finally:
if 'client' in locals(): if "client" in locals():
client.close() client.close()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,24 +1,24 @@
"""Main songbox API Client""" """Main songbox API Client"""
from typing import Any, Dict, Optional
import httpx import httpx
from typing import Optional, Dict, Any
from .auth import AuthClient from .clients import AuthClient, MusicClient, UserClient
from .user import UserClient from .exceptions import APIError, songboxError
from .music import MusicClient
from .exceptions import songboxError, APIError
class SongboxClient: class SongboxClient:
"""Main client for interacting with the songbox API. """Main client for interacting with the songbox API.
This client provides access to all songbox API endpoints through This client provides access to all songbox API endpoints through
modular sub-clients for authentication, user management, and music. modular sub-clients for authentication, user management, and music.
Args: Args:
base_url: The base URL for the songbox API base_url: The base URL for the songbox API
timeout: Request timeout in seconds (default: 30) timeout: Request timeout in seconds (default: 30)
headers: Additional headers to include with requests headers: Additional headers to include with requests
Example: Example:
>>> client = SongboxClient() >>> client = SongboxClient()
>>> # Authenticate >>> # Authenticate
@ -29,62 +29,62 @@ class SongboxClient:
>>> user_info = client.user.get_me() >>> user_info = client.user.get_me()
>>> print(user_info) >>> print(user_info)
""" """
def __init__( def __init__(
self, self,
base_url: str = "https://lavafoshi.online", base_url: str = "https://lavafoshi.online",
timeout: float = 30.0, timeout: float = 30.0,
headers: Optional[Dict[str, str]] = None headers: Optional[Dict[str, str]] = None,
): ):
self.base_url = base_url.rstrip('/') self.base_url = base_url.rstrip("/")
self._token: Optional[str] = None self._token: Optional[str] = None
# Default headers # Default headers
default_headers = { default_headers = {
"User-Agent": "songbox/2.1.4 (songbox; Python) httpx", "User-Agent": "songbox/2.1.4 (songbox; Python) httpx",
"Accept": "*/*", "Accept": "*/*",
"Accept-Language": "en-US;q=1.0", "Accept-Language": "en-US;q=1.0",
"Accept-Encoding": "gzip;q=1.0, compress;q=0.5", "Accept-Encoding": "gzip;q=1.0, compress;q=0.5",
"Connection": "keep-alive" "Connection": "keep-alive",
} }
if headers: if headers:
default_headers.update(headers) default_headers.update(headers)
self._http_client = httpx.Client( self._http_client = httpx.Client(
base_url=self.base_url, base_url=self.base_url,
timeout=timeout, timeout=timeout,
headers=default_headers, headers=default_headers,
follow_redirects=True follow_redirects=True,
) )
self.auth = AuthClient(self) self.auth = AuthClient(self)
self.user = UserClient(self) self.user = UserClient(self)
self.music = MusicClient(self) self.music = MusicClient(self)
def set_token(self, token: str) -> None: def set_token(self, token: str) -> None:
"""Set the authentication token for API requests. """Set the authentication token for API requests.
Args: Args:
token: JWT token obtained from authentication token: JWT token obtained from authentication
""" """
self._token = token self._token = token
self._http_client.headers["Authorization"] = f"Bearer {token}" self._http_client.headers["Authorization"] = f"Bearer {token}"
def clear_token(self) -> None: def clear_token(self) -> None:
"""Clear the authentication token.""" """Clear the authentication token."""
self._token = None self._token = None
if "Authorization" in self._http_client.headers: if "Authorization" in self._http_client.headers:
del self._http_client.headers["Authorization"] del self._http_client.headers["Authorization"]
@property @property
def token(self) -> Optional[str]: def token(self) -> Optional[str]:
"""Get the current authentication token.""" """Get the current authentication token."""
return self._token return self._token
@property @property
def is_authenticated(self) -> bool: def is_authenticated(self) -> bool:
"""Check if the client is authenticated.""" """Check if the client is authenticated."""
return self._token is not None return self._token is not None
def request( def request(
self, self,
method: str, method: str,
@ -93,10 +93,10 @@ class SongboxClient:
data: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None,
json: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None,
require_auth: bool = True require_auth: bool = True,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make an HTTP request to the songbox API. """Make an HTTP request to the songbox API.
Args: Args:
method: HTTP method (GET, POST, etc.) method: HTTP method (GET, POST, etc.)
endpoint: API endpoint (without base URL) endpoint: API endpoint (without base URL)
@ -105,19 +105,19 @@ class SongboxClient:
json: JSON data for POST requests json: JSON data for POST requests
headers: Additional headers headers: Additional headers
require_auth: Whether authentication is required require_auth: Whether authentication is required
Returns: Returns:
JSON response data JSON response data
Raises: Raises:
songboxError: If authentication is required but not provided songboxError: If authentication is required but not provided
APIError: If the API returns an error response APIError: If the API returns an error response
""" """
if require_auth and not self.is_authenticated: if require_auth and not self.is_authenticated:
raise songboxError("Authentication required for this endpoint") raise songboxError("Authentication required for this endpoint")
url = endpoint if endpoint.startswith('http') else f"/api{endpoint}" url = endpoint if endpoint.startswith("http") else f"/api{endpoint}"
try: try:
response = self._http_client.request( response = self._http_client.request(
method=method, method=method,
@ -125,34 +125,34 @@ class SongboxClient:
params=params, params=params,
data=data, data=data,
json=json, json=json,
headers=headers headers=headers,
) )
response.raise_for_status() response.raise_for_status()
try: try:
return response.json() return response.json()
except ValueError: except ValueError:
return {} return {}
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
try: try:
error_data = e.response.json() error_data = e.response.json()
message = error_data.get('message', f'HTTP {e.response.status_code}') message = error_data.get("message", f"HTTP {e.response.status_code}")
except ValueError: except ValueError:
message = f'HTTP {e.response.status_code}' message = f"HTTP {e.response.status_code}"
raise APIError(message, status_code=e.response.status_code) raise APIError(message, status_code=e.response.status_code)
except httpx.RequestError as e: except httpx.RequestError as e:
raise songboxError(f"Request failed: {str(e)}") raise songboxError(f"Request failed: {str(e)}")
def close(self) -> None: def close(self) -> None:
"""Close the HTTP client.""" """Close the HTTP client."""
self._http_client.close() self._http_client.close()
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
self.close() self.close()

View File

@ -0,0 +1,6 @@
from .music import MusicClient
from .user import UserClient
from .auth import AuthClient
__all__ = ["AuthClient", "UserClient", "MusicClient"]

View File

@ -1,7 +1,7 @@
"""Authentication client for songbox API""" """Authentication client for songbox API"""
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from .exceptions import AuthenticationError, ValidationError from ..exceptions import AuthenticationError, ValidationError
class AuthClient: class AuthClient:

View File

@ -5,7 +5,7 @@ import httpx
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from .exceptions import AuthenticationError, NotFoundError, ValidationError from ..exceptions import AuthenticationError, NotFoundError, ValidationError
class MusicClient: class MusicClient:

View File

@ -1,7 +1,7 @@
"""User client for songbox API""" """User client for songbox API"""
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from .exceptions import AuthenticationError, NotFoundError from ..exceptions import AuthenticationError, NotFoundError
class UserClient: class UserClient:

View File

@ -5,29 +5,30 @@ from typing import Optional
class songboxError(Exception): class songboxError(Exception):
"""Base exception for all songbox API errors.""" """Base exception for all songbox API errors."""
pass pass
class APIError(songboxError): class APIError(songboxError):
"""Exception raised when the API returns an error response. """Exception raised when the API returns an error response.
Args: Args:
message: Error message message: Error message
status_code: HTTP status code status_code: HTTP status code
response_data: Raw response data from the API response_data: Raw response data from the API
""" """
def __init__( def __init__(
self, self,
message: str, message: str,
status_code: Optional[int] = None, status_code: Optional[int] = None,
response_data: Optional[dict] = None response_data: Optional[dict] = None,
): ):
super().__init__(message) super().__init__(message)
self.message = message self.message = message
self.status_code = status_code self.status_code = status_code
self.response_data = response_data or {} self.response_data = response_data or {}
def __str__(self) -> str: def __str__(self) -> str:
if self.status_code: if self.status_code:
return f"API Error {self.status_code}: {self.message}" return f"API Error {self.status_code}: {self.message}"
@ -36,19 +37,23 @@ class APIError(songboxError):
class AuthenticationError(songboxError): class AuthenticationError(songboxError):
"""Exception raised for authentication-related errors.""" """Exception raised for authentication-related errors."""
pass pass
class RateLimitError(APIError): class RateLimitError(APIError):
"""Exception raised when rate limit is exceeded.""" """Exception raised when rate limit is exceeded."""
pass pass
class NotFoundError(APIError): class NotFoundError(APIError):
"""Exception raised when a resource is not found (404).""" """Exception raised when a resource is not found (404)."""
pass pass
class ValidationError(songboxError): class ValidationError(songboxError):
"""Exception raised for input validation errors.""" """Exception raised for input validation errors."""
pass
pass