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:
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"]
|
@ -1,2 +1,2 @@
|
|||||||
httpx==0.28.1
|
|
||||||
boto3==1.38.32
|
boto3==1.38.32
|
||||||
|
httpx==0.28.1
|
||||||
|
7
setup.py
7
setup.py
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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:
|
||||||
@ -33,25 +33,25 @@ Examples:
|
|||||||
|
|
||||||
# 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")
|
||||||
@ -61,14 +61,22 @@ Examples:
|
|||||||
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")
|
||||||
@ -109,8 +117,12 @@ Examples:
|
|||||||
|
|
||||||
# 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,7 +171,9 @@ 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)
|
||||||
@ -169,7 +183,9 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
|
|||||||
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)
|
||||||
@ -257,7 +273,6 @@ 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()
|
||||||
@ -269,6 +284,7 @@ def main():
|
|||||||
|
|
||||||
# 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
|
||||||
@ -284,12 +300,18 @@ def main():
|
|||||||
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)
|
||||||
|
|
||||||
@ -300,7 +322,7 @@ 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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"""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:
|
||||||
@ -34,9 +34,9 @@ class SongboxClient:
|
|||||||
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
|
||||||
@ -45,7 +45,7 @@ class SongboxClient:
|
|||||||
"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:
|
||||||
@ -54,7 +54,7 @@ class SongboxClient:
|
|||||||
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)
|
||||||
@ -93,7 +93,7 @@ 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.
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ class SongboxClient:
|
|||||||
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(
|
||||||
@ -125,7 +125,7 @@ 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()
|
||||||
@ -138,9 +138,9 @@ class SongboxClient:
|
|||||||
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)
|
||||||
|
|
||||||
|
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"""
|
"""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:
|
@ -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:
|
@ -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:
|
@ -5,6 +5,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ class APIError(songboxError):
|
|||||||
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
|
||||||
@ -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
|
Reference in New Issue
Block a user