Skip to content

Enhance SDK with type safety, error handling and resource management improvements #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 47 additions & 8 deletions src/lighthouseweb3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,46 @@ def upload(self, source: str, tag: str = ''):
Upload a file or directory to the Lighthouse.

:param source: str, path to file or directory
:param tag: str, optional tag for the upload
:return: t.Upload, the upload result
:raises ValueError: If source path is invalid or doesn't exist
:raises Exception: If upload fails
"""
if not source or not isinstance(source, str):
raise ValueError("Source path must be a non-empty string")

if not os.path.exists(source):
raise ValueError(f"Source path does not exist: {source}")

try:
return d.upload(source, self.token, tag)
except Exception as e:
raise e
raise Exception(f"Upload failed: {str(e)}")

def uploadBlob(self, source: io.BufferedReader, filename: str, tag: str = ''):
"""
Upload Blob a file or directory to the Lighthouse.

:param source: str, path to file or directory
:param source: io.BufferedReader, file-like object to upload
:param filename: str, name of the file to be uploaded
:param tag: str, optional tag for the upload
:return: t.Upload, the upload result
:raises TypeError: If source is not a proper file-like object
:raises ValueError: If filename is invalid
"""
if not isinstance(source, io.BufferedReader):
raise TypeError("source must be an instance of io.BufferedReader")

if not filename or not isinstance(filename, str):
raise ValueError("filename must be a non-empty string")

if not (hasattr(source, 'read') and hasattr(source, 'close')):
raise TypeError("source must have 'read' and 'close' methods")

try:
return d.uploadBlob(source, filename, self.token, tag)
except Exception as e:
raise e
raise Exception(f"Failed to upload blob: {str(e)}")

@staticmethod
def downloadBlob(dist: io.BufferedWriter, cid: str, chunk_size=1024*1024*10):
Expand All @@ -48,13 +68,19 @@ def downloadBlob(dist: io.BufferedWriter, cid: str, chunk_size=1024*1024*10):
:param cid: str, Content Identifier for the data to be downloaded
:param chunk_size: int, size of chunks in which the file will be downloaded (default: 10MB)
:return: t.Upload, the download result
:raises TypeError: If dist doesn't have required write and close methods
:raises ValueError: If cid is empty or invalid
"""
if not (hasattr(dist, 'read') and hasattr(dist, 'close')):
raise TypeError("source must have 'read' and 'close' methods")
if not (hasattr(dist, 'write') and hasattr(dist, 'close')):
raise TypeError("dist must have 'write' and 'close' methods")

if not cid or not isinstance(cid, str):
raise ValueError("Invalid CID provided")

try:
return _download.download_file_into_writable(cid, dist, chunk_size)
except Exception as e:
raise e
raise Exception(f"Failed to download file: {str(e)}")

@staticmethod
def getDealStatus(cid: str):
Expand All @@ -63,11 +89,16 @@ def getDealStatus(cid: str):

:param cid: str, content identifier
:return: List[t.DealData], list of deal data
:raises ValueError: If CID is invalid
:raises Exception: If fetching deal status fails
"""
if not cid or not isinstance(cid, str):
raise ValueError("CID must be a non-empty string")

try:
return deal_status.get_deal_status(cid)
except Exception as e:
raise e
raise Exception(f"Failed to get deal status: {str(e)}")

@staticmethod
def getUploads(publicKey: str, pageNo: int = 1):
Expand All @@ -77,11 +108,19 @@ def getUploads(publicKey: str, pageNo: int = 1):
:param publicKey: str, public key
:param pageNo: int, page number (default: 1)
:return: List[t.DealData], list of deal data
:raises ValueError: If publicKey is invalid or pageNo is less than 1
:raises Exception: If fetching uploads fails
"""
if not publicKey or not isinstance(publicKey, str):
raise ValueError("Public key must be a non-empty string")

if not isinstance(pageNo, int) or pageNo < 1:
raise ValueError("Page number must be a positive integer")

try:
return getUploads.get_uploads(publicKey, pageNo)
except Exception as e:
raise e
raise Exception(f"Failed to get uploads: {str(e)}")

@staticmethod
def download(cid: str):
Expand Down
145 changes: 109 additions & 36 deletions src/lighthouseweb3/functions/axios.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,156 @@

from io import BufferedReader
import json
from typing import Optional, Dict, Any, Union
import requests as req
from requests.exceptions import RequestException
from . import utils


class Axios:
"""It's not axios, it's just a custom extensible wrapper for requests"""
"""
A custom extensible wrapper for requests library.
Provides simplified HTTP client functionality with proper error handling.
"""

def __init__(self, url: str):
"""
Initialize Axios instance.

:param url: Base URL for requests
:raises ValueError: If URL is empty or invalid
"""
if not url or not isinstance(url, str):
raise ValueError("URL must be a non-empty string")
self.url = url

def parse_url_query(self, query):
def parse_url_query(self, query: Optional[Dict[str, Any]]) -> None:
"""
Parse and append query parameters to URL.

:param query: Dictionary of query parameters
:raises ValueError: If query parameters are invalid
"""
try:
if query is not None and isinstance(query, dict):
if query is not None:
if not isinstance(query, dict):
raise ValueError("Query must be a dictionary")
for key, value in query.items():
self.url += f"&{key}={value}"
except Exception as e:
raise e
raise ValueError(f"Failed to parse query parameters: {str(e)}")

def get(self, headers = None, **kwargs) :
def get(self, headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
"""
Perform GET request.

:param headers: Request headers
:param kwargs: Additional parameters including query
:return: JSON response
:raises RequestException: If request fails
"""
try:
self.parse_url_query(kwargs.get("query", None))
r = req.get(self.url, headers=headers)
self.parse_url_query(kwargs.get("query"))
r = req.get(self.url, headers=headers, timeout=30) # Added timeout
r.raise_for_status()
return r.json()
except Exception as e:
raise e
except req.exceptions.JSONDecodeError as e:
raise ValueError(f"Failed to decode JSON response: {str(e)}")
except RequestException as e:
raise RequestException(f"GET request failed: {str(e)}")

def post(
self, body=None, headers= None, **kwargs
):
def post(self, body: Optional[Any] = None, headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
"""
Perform POST request.

:param body: Request body
:param headers: Request headers
:param kwargs: Additional parameters including query
:return: JSON response
:raises RequestException: If request fails
"""
try:
self.parse_url_query(kwargs.get("query", None))
r = req.post(self.url, data=body, headers=headers)
self.parse_url_query(kwargs.get("query"))
r = req.post(self.url, data=body, headers=headers, timeout=30) # Added timeout
r.raise_for_status()
return r.json()
except Exception as e:
raise e
except req.exceptions.JSONDecodeError as e:
raise ValueError(f"Failed to decode JSON response: {str(e)}")
except RequestException as e:
raise RequestException(f"POST request failed: {str(e)}")

def post_files(
self, file, headers = None, **kwargs
) :
def post_files(self, file: Dict[str, Any], headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
"""
Upload files via POST request.

:param file: File information dictionary
:param headers: Request headers
:param kwargs: Additional parameters including query
:return: JSON response
:raises RequestException: If request fails
"""
files = None
try:
self.parse_url_query(kwargs.get("query", None))
self.parse_url_query(kwargs.get("query"))
files = utils.read_files_for_upload(file)
r = req.post(self.url, headers=headers, files=files)
r = req.post(self.url, headers=headers, files=files, timeout=30) # Added timeout
r.raise_for_status()
utils.close_files_after_upload(files)

# Always ensure files are closed
if files:
utils.close_files_after_upload(files)

try:
return r.json()
except Exception:
except req.exceptions.JSONDecodeError:
# Handle special case where response contains multiple lines
temp = r.text.split("\n")
return json.loads(temp[len(temp) - 2])
return json.loads(temp[-2]) # Use -2 index instead of len(temp) - 2
except Exception as e:
utils.close_files_after_upload(files)
raise e
# Ensure files are closed even if an error occurs
if files:
utils.close_files_after_upload(files)
raise RequestException(f"File upload failed: {str(e)}")

def post_blob(self, file: BufferedReader, filename: str, headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
"""
Upload blob data via POST request.

:param file: BufferedReader instance containing file data
:param filename: Name of the file
:param headers: Request headers
:param kwargs: Additional parameters including query
:return: JSON response
:raises RequestException: If request fails
:raises ValueError: If file or filename is invalid
"""
if not isinstance(file, BufferedReader):
raise ValueError("file must be a BufferedReader instance")
if not filename or not isinstance(filename, str):
raise ValueError("filename must be a non-empty string")

def post_blob(
self, file: BufferedReader, filename: str, headers = None, **kwargs
) :
try:
self.parse_url_query(kwargs.get("query", None))
self.parse_url_query(kwargs.get("query"))
files = [(
"file",
(
utils.extract_file_name(filename),
file.read(),
"application/octet-stream",
),
),]
r = req.post(self.url, headers=headers, files=files)
)]

r = req.post(self.url, headers=headers, files=files, timeout=30) # Added timeout
r.raise_for_status()
file.close()

try:
return r.json()
except Exception:
except req.exceptions.JSONDecodeError:
# Handle special case where response contains multiple lines
temp = r.text.split("\n")
return json.loads(temp[len(temp) - 2])
return json.loads(temp[-2]) # Use -2 index instead of len(temp) - 2
except Exception as e:
raise RequestException(f"Blob upload failed: {str(e)}")
finally:
# Always ensure file is closed
file.close()
raise e
Loading