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:
2
.gitignore
vendored
2
.gitignore
vendored
@ -170,4 +170,4 @@ cython_debug/
|
||||
|
||||
venv/*
|
||||
Documents.md
|
||||
test.py
|
||||
test.py
|
||||
|
35
.pre-commit-config.yaml
Normal file
35
.pre-commit-config.yaml
Normal 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"]
|
12
README.md
12
README.md
@ -204,18 +204,18 @@ try:
|
||||
# Step 1: Initiate login
|
||||
verify_id = client.auth.login("960", "7777777")
|
||||
print(f"OTP sent! Verification ID: {verify_id}")
|
||||
|
||||
|
||||
# Step 2: Get OTP from user
|
||||
otp = input("Enter OTP: ")
|
||||
|
||||
|
||||
# Step 3: Verify OTP and get token
|
||||
token = client.auth.verify_otp(otp, verify_id)
|
||||
print(f"Login successful! Token: {token}")
|
||||
|
||||
|
||||
# Step 4: Use the API
|
||||
user_info = client.user.get_me()
|
||||
print(f"Welcome, {user_info['username']}!")
|
||||
|
||||
|
||||
except AuthenticationError as e:
|
||||
print(f"Authentication failed: {e}")
|
||||
finally:
|
||||
@ -232,7 +232,7 @@ for category in results:
|
||||
print(f"\n{category['title']}:")
|
||||
for item in category['items'][:3]: # Show top 3
|
||||
print(f" {item['heading']} - {item['sub_heading']}")
|
||||
|
||||
|
||||
# Get detailed info if it's a song
|
||||
if category['title'] == 'Songs':
|
||||
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.
|
||||
|
@ -1,2 +1,2 @@
|
||||
boto3==1.38.32
|
||||
httpx==0.28.1
|
||||
boto3==1.38.32
|
9
setup.py
9
setup.py
@ -1,6 +1,6 @@
|
||||
"""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:
|
||||
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:
|
||||
# requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")]
|
||||
|
||||
requirements = [
|
||||
"httpx==0.28.1",
|
||||
"boto3==1.38.32"
|
||||
]
|
||||
requirements = ["httpx==0.28.1", "boto3==1.38.32"]
|
||||
|
||||
setup(
|
||||
name="songbox",
|
||||
@ -59,4 +56,4 @@ setup(
|
||||
"Source": "https://git.cubable.date/CustomIcon/songbox",
|
||||
"Documentation": "https://git.cubable.date/CustomIcon/songbox/src/branch/master/README.md",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
@ -4,15 +4,13 @@ A Python client library for the songbox music streaming API.
|
||||
"""
|
||||
|
||||
from .client import SongboxClient
|
||||
from .auth import AuthClient
|
||||
from .user import UserClient
|
||||
from .music import MusicClient
|
||||
from .clients import AuthClient, MusicClient, UserClient
|
||||
from .exceptions import (
|
||||
songboxError,
|
||||
AuthenticationError,
|
||||
APIError,
|
||||
AuthenticationError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
NotFoundError
|
||||
songboxError,
|
||||
)
|
||||
|
||||
__version__ = "1.0.0"
|
||||
@ -29,4 +27,4 @@ __all__ = [
|
||||
"APIError",
|
||||
"RateLimitError",
|
||||
"NotFoundError",
|
||||
]
|
||||
]
|
||||
|
@ -3,9 +3,9 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from .client import SongboxClient
|
||||
from .exceptions import songboxError, AuthenticationError
|
||||
from .exceptions import AuthenticationError, songboxError
|
||||
|
||||
|
||||
def create_parser() -> argparse.ArgumentParser:
|
||||
@ -20,98 +20,110 @@ Examples:
|
||||
songbox auth login --country-code 960 --mobile 7777777
|
||||
songbox auth verify --otp 123456 --verify-id <verify_id>
|
||||
songbox user me
|
||||
|
||||
|
||||
# Search for music
|
||||
songbox music search "huttaa"
|
||||
|
||||
|
||||
# Get album information
|
||||
songbox music album 808
|
||||
|
||||
|
||||
# Download a song
|
||||
songbox music download "https://example.com/song.mp3"
|
||||
songbox music download "https://example.com/song.mp3" --path ./my_music
|
||||
|
||||
|
||||
# Get user's following list
|
||||
songbox user following
|
||||
"""
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
"--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(
|
||||
"--base-url",
|
||||
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(
|
||||
"--format",
|
||||
choices=["json", "table", "simple"],
|
||||
default="simple",
|
||||
help="Output format (default: simple)"
|
||||
help="Output format (default: simple)",
|
||||
)
|
||||
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
|
||||
# Auth commands
|
||||
auth_parser = subparsers.add_parser("auth", help="Authentication commands")
|
||||
auth_subparsers = auth_parser.add_subparsers(dest="auth_command")
|
||||
|
||||
|
||||
# Login command
|
||||
login_parser = auth_subparsers.add_parser("login", help="Initiate login with mobile number")
|
||||
login_parser.add_argument("--country-code", required=True, help="Country code (e.g., 960)")
|
||||
login_parser = auth_subparsers.add_parser(
|
||||
"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")
|
||||
|
||||
|
||||
# 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("--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
|
||||
auth_subparsers.add_parser("refresh", help="Refresh authentication token")
|
||||
|
||||
|
||||
# Logout command
|
||||
auth_subparsers.add_parser("logout", help="Logout (clear token)")
|
||||
|
||||
|
||||
# User commands
|
||||
user_parser = subparsers.add_parser("user", help="User commands")
|
||||
user_subparsers = user_parser.add_subparsers(dest="user_command")
|
||||
|
||||
|
||||
user_subparsers.add_parser("me", help="Get current user information")
|
||||
user_subparsers.add_parser("following", help="Get following list")
|
||||
|
||||
|
||||
# Music commands
|
||||
music_parser = subparsers.add_parser("music", help="Music commands")
|
||||
music_subparsers = music_parser.add_subparsers(dest="music_command")
|
||||
|
||||
|
||||
# Search command
|
||||
search_parser = music_subparsers.add_parser("search", help="Search for music")
|
||||
search_parser.add_argument("query", help="Search query")
|
||||
|
||||
|
||||
# Album command
|
||||
album_parser = music_subparsers.add_parser("album", help="Get album information")
|
||||
album_parser.add_argument("album_id", help="Album ID")
|
||||
|
||||
|
||||
# Song command
|
||||
song_parser = music_subparsers.add_parser("song", help="Get song information")
|
||||
song_parser.add_argument("song_id", help="Song ID")
|
||||
|
||||
|
||||
# Artist command
|
||||
artist_parser = music_subparsers.add_parser("artist", help="Get artist information")
|
||||
artist_parser.add_argument("artist_id", help="Artist ID")
|
||||
|
||||
|
||||
# Trending command
|
||||
trending_parser = music_subparsers.add_parser("trending", help="Get trending music")
|
||||
trending_parser.add_argument("--limit", type=int, help="Limit number of results")
|
||||
|
||||
|
||||
# Download command
|
||||
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("--path", help="Download directory path (default: ./downloads)")
|
||||
|
||||
download_parser.add_argument(
|
||||
"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
|
||||
|
||||
|
||||
@ -159,21 +171,25 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
|
||||
try:
|
||||
verify_id = client.auth.login(args.country_code, args.mobile)
|
||||
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:
|
||||
print(f"Login failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
elif args.auth_command == "verify":
|
||||
try:
|
||||
token = client.auth.verify_otp(args.otp, args.verify_id)
|
||||
print(f"Authentication successful!")
|
||||
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:
|
||||
print(f"Verification failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
elif args.auth_command == "refresh":
|
||||
try:
|
||||
new_token = client.auth.refresh_token()
|
||||
@ -182,7 +198,7 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
|
||||
except Exception as e:
|
||||
print(f"Token refresh failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
elif args.auth_command == "logout":
|
||||
client.auth.logout()
|
||||
print("Logged out successfully!")
|
||||
@ -197,7 +213,7 @@ def handle_user_commands(client: SongboxClient, args) -> None:
|
||||
except Exception as e:
|
||||
print(f"Failed to get user info: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
elif args.user_command == "following":
|
||||
try:
|
||||
following = client.user.get_following()
|
||||
@ -222,7 +238,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
|
||||
except Exception as e:
|
||||
print(f"Search failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
elif args.music_command == "album":
|
||||
try:
|
||||
album = client.music.get_album(args.album_id)
|
||||
@ -230,7 +246,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
|
||||
except Exception as e:
|
||||
print(f"Failed to get album: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
elif args.music_command == "song":
|
||||
try:
|
||||
song = client.music.get_song(args.song_id)
|
||||
@ -238,7 +254,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
|
||||
except Exception as e:
|
||||
print(f"Failed to get song: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
elif args.music_command == "artist":
|
||||
try:
|
||||
artist = client.music.get_artist(args.artist_id)
|
||||
@ -246,7 +262,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
|
||||
except Exception as e:
|
||||
print(f"Failed to get artist: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
elif args.music_command == "download":
|
||||
try:
|
||||
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)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point."""
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Get token from args or environment
|
||||
import os
|
||||
|
||||
token = args.token or os.getenv("songbox_TOKEN")
|
||||
|
||||
|
||||
# Create client
|
||||
try:
|
||||
client = SongboxClient(base_url=args.base_url)
|
||||
|
||||
|
||||
# Set token if provided
|
||||
if token:
|
||||
client.set_token(token)
|
||||
|
||||
|
||||
# Handle commands
|
||||
if args.command == "auth":
|
||||
handle_auth_commands(client, args)
|
||||
elif args.command == "user":
|
||||
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)
|
||||
handle_user_commands(client, args)
|
||||
elif args.command == "music":
|
||||
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)
|
||||
handle_music_commands(client, args)
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nOperation cancelled.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@ -300,9 +322,9 @@ def main():
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if 'client' in locals():
|
||||
if "client" in locals():
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
@ -1,24 +1,24 @@
|
||||
"""Main songbox API Client"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from typing import Optional, Dict, Any
|
||||
from .auth import AuthClient
|
||||
from .user import UserClient
|
||||
from .music import MusicClient
|
||||
from .exceptions import songboxError, APIError
|
||||
|
||||
from .clients import AuthClient, MusicClient, UserClient
|
||||
from .exceptions import APIError, songboxError
|
||||
|
||||
|
||||
class SongboxClient:
|
||||
"""Main client for interacting with the songbox API.
|
||||
|
||||
|
||||
This client provides access to all songbox API endpoints through
|
||||
modular sub-clients for authentication, user management, and music.
|
||||
|
||||
|
||||
Args:
|
||||
base_url: The base URL for the songbox API
|
||||
timeout: Request timeout in seconds (default: 30)
|
||||
headers: Additional headers to include with requests
|
||||
|
||||
|
||||
Example:
|
||||
>>> client = SongboxClient()
|
||||
>>> # Authenticate
|
||||
@ -29,62 +29,62 @@ class SongboxClient:
|
||||
>>> user_info = client.user.get_me()
|
||||
>>> print(user_info)
|
||||
"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str = "https://lavafoshi.online",
|
||||
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
|
||||
|
||||
|
||||
# Default headers
|
||||
default_headers = {
|
||||
"User-Agent": "songbox/2.1.4 (songbox; Python) httpx",
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "en-US;q=1.0",
|
||||
"Accept-Encoding": "gzip;q=1.0, compress;q=0.5",
|
||||
"Connection": "keep-alive"
|
||||
"Connection": "keep-alive",
|
||||
}
|
||||
|
||||
|
||||
if headers:
|
||||
default_headers.update(headers)
|
||||
self._http_client = httpx.Client(
|
||||
base_url=self.base_url,
|
||||
timeout=timeout,
|
||||
headers=default_headers,
|
||||
follow_redirects=True
|
||||
follow_redirects=True,
|
||||
)
|
||||
self.auth = AuthClient(self)
|
||||
self.user = UserClient(self)
|
||||
self.music = MusicClient(self)
|
||||
|
||||
|
||||
def set_token(self, token: str) -> None:
|
||||
"""Set the authentication token for API requests.
|
||||
|
||||
|
||||
Args:
|
||||
token: JWT token obtained from authentication
|
||||
"""
|
||||
self._token = token
|
||||
self._http_client.headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
|
||||
def clear_token(self) -> None:
|
||||
"""Clear the authentication token."""
|
||||
self._token = None
|
||||
if "Authorization" in self._http_client.headers:
|
||||
del self._http_client.headers["Authorization"]
|
||||
|
||||
|
||||
@property
|
||||
def token(self) -> Optional[str]:
|
||||
"""Get the current authentication token."""
|
||||
return self._token
|
||||
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
"""Check if the client is authenticated."""
|
||||
return self._token is not None
|
||||
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
@ -93,10 +93,10 @@ class SongboxClient:
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
json: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
require_auth: bool = True
|
||||
require_auth: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Make an HTTP request to the songbox API.
|
||||
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
endpoint: API endpoint (without base URL)
|
||||
@ -105,19 +105,19 @@ class SongboxClient:
|
||||
json: JSON data for POST requests
|
||||
headers: Additional headers
|
||||
require_auth: Whether authentication is required
|
||||
|
||||
|
||||
Returns:
|
||||
JSON response data
|
||||
|
||||
|
||||
Raises:
|
||||
songboxError: If authentication is required but not provided
|
||||
APIError: If the API returns an error response
|
||||
"""
|
||||
if require_auth and not self.is_authenticated:
|
||||
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:
|
||||
response = self._http_client.request(
|
||||
method=method,
|
||||
@ -125,34 +125,34 @@ class SongboxClient:
|
||||
params=params,
|
||||
data=data,
|
||||
json=json,
|
||||
headers=headers
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError:
|
||||
return {}
|
||||
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
try:
|
||||
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:
|
||||
message = f'HTTP {e.response.status_code}'
|
||||
|
||||
message = f"HTTP {e.response.status_code}"
|
||||
|
||||
raise APIError(message, status_code=e.response.status_code)
|
||||
|
||||
|
||||
except httpx.RequestError as e:
|
||||
raise songboxError(f"Request failed: {str(e)}")
|
||||
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the HTTP client."""
|
||||
self._http_client.close()
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
self.close()
|
||||
|
6
songbox/clients/__init__.py
Normal file
6
songbox/clients/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from .music import MusicClient
|
||||
from .user import UserClient
|
||||
from .auth import AuthClient
|
||||
|
||||
|
||||
__all__ = ["AuthClient", "UserClient", "MusicClient"]
|
@ -1,7 +1,7 @@
|
||||
"""Authentication client for songbox API"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from .exceptions import AuthenticationError, ValidationError
|
||||
from ..exceptions import AuthenticationError, ValidationError
|
||||
|
||||
|
||||
class AuthClient:
|
@ -5,7 +5,7 @@ import httpx
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
from typing import Dict, Any, List, Optional
|
||||
from .exceptions import AuthenticationError, NotFoundError, ValidationError
|
||||
from ..exceptions import AuthenticationError, NotFoundError, ValidationError
|
||||
|
||||
|
||||
class MusicClient:
|
@ -1,7 +1,7 @@
|
||||
"""User client for songbox API"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from .exceptions import AuthenticationError, NotFoundError
|
||||
from ..exceptions import AuthenticationError, NotFoundError
|
||||
|
||||
|
||||
class UserClient:
|
@ -5,29 +5,30 @@ from typing import Optional
|
||||
|
||||
class songboxError(Exception):
|
||||
"""Base exception for all songbox API errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class APIError(songboxError):
|
||||
"""Exception raised when the API returns an error response.
|
||||
|
||||
|
||||
Args:
|
||||
message: Error message
|
||||
status_code: HTTP status code
|
||||
response_data: Raw response data from the API
|
||||
"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: Optional[int] = None,
|
||||
response_data: Optional[dict] = None
|
||||
self,
|
||||
message: str,
|
||||
status_code: Optional[int] = None,
|
||||
response_data: Optional[dict] = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
self.response_data = response_data or {}
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.status_code:
|
||||
return f"API Error {self.status_code}: {self.message}"
|
||||
@ -36,19 +37,23 @@ class APIError(songboxError):
|
||||
|
||||
class AuthenticationError(songboxError):
|
||||
"""Exception raised for authentication-related errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RateLimitError(APIError):
|
||||
"""Exception raised when rate limit is exceeded."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(APIError):
|
||||
"""Exception raised when a resource is not found (404)."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(songboxError):
|
||||
"""Exception raised for input validation errors."""
|
||||
pass
|
||||
|
||||
pass
|
||||
|
Reference in New Issue
Block a user