Skip to content
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

[UPY-7] Support async operations #6

Merged
merged 1 commit into from
Feb 13, 2025
Merged
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
51 changes: 43 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,57 @@ except Exception as e:

## API Reference

### UTApi Methods
### Client Classes

All methods are defined in [`upyloadthing/client.py`](upyloadthing/client.py):
Both clients provide the same methods with identical parameters, but different execution patterns:

- `upload_files(files)` - Upload one or more files
- `delete_files(keys, key_type='file_key')` - Delete files by key or custom ID
- `list_files(limit=None, offset=None)` - List uploaded files
- `get_usage_info()` - Get account usage statistics
#### UTApi (Synchronous)
```python
from upyloadthing import UTApi

api = UTApi(UTApiOptions(token="your-token"))
result = api.upload_files(file)
```

#### AsyncUTApi (Asynchronous)
```python
from upyloadthing import AsyncUTApi

api = AsyncUTApi(UTApiOptions(token="your-token"))
result = await api.upload_files(file)
```

### Methods

Both clients provide these methods:

- `upload_files(files: BinaryIO | List[BinaryIO], content_disposition: str = "inline", acl: str | None = "public-read") -> List[UploadResult]`
- Upload one or more files
- Returns list of upload results

- `delete_files(keys: str | List[str], key_type: str = "file_key") -> DeleteFileResponse`
- Delete one or more files by key or custom ID
- Returns deletion result

- `list_files(limit: int | None = None, offset: int | None = None) -> ListFileResponse`
- List uploaded files with optional pagination
- Returns file listing

- `get_usage_info() -> UsageInfoResponse`
- Get account usage statistics
- Returns usage information
Comment on lines +123 to +141
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Method signatures are well documented, but error handling needs update.

The method signatures with type hints are clear and helpful. However, there's a discrepancy in the error handling section.

The error handling example uses requests.exceptions.HTTPError but the client uses httpx. Update the error handling section to use the correct exception:

-from requests.exceptions import HTTPError
+from httpx import HTTPError

try:
    api.upload_files(file)
except HTTPError as e:

Committable suggestion skipped: line range outside the PR's diff.


### Response Models

All response models are defined in [`upyloadthing/schemas.py`](upyloadthing/schemas.py):
All response models are defined in `upyloadthing/schemas.py`:

- `UploadResult` - File upload result containing:
- `ufs_url: str`
- `file_key: str`
- `name: str`
- `size: int`
- `type: str`
- `url: str`
- `ufs_url: str`
- `app_url: str`
- `file_hash: str`
- `server_data: Dict | None`
Expand Down
66 changes: 66 additions & 0 deletions examples/async_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import asyncio
from io import BytesIO
from typing import List

from upyloadthing.async_client import AsyncUTApi
from upyloadthing.schemas import UploadResult


async def main():
print("🚀 UploadThing API Demo (Async)\n")

# Initialize the client
api = AsyncUTApi()

# Get usage info
print("📊 Getting usage info...")
usage_info = await api.get_usage_info()
print(f"Total bytes used: {usage_info.total_bytes}")
print(f"Files uploaded: {usage_info.files_uploaded}")
print(f"Storage limit: {usage_info.limit_bytes}\n")

# List files
print("📋 Listing files...")
file_list = await api.list_files(limit=5)
print(
f"Fetched {len(file_list.files)} files, has more: {file_list.has_more}"
)
for file in file_list.files:
print(file)
print()

# Prepare test files
print("📤 Uploading test images...")

# Prepare PNG file
with open("./examples/test.png", "rb") as f:
image_content = f.read()
png_file = BytesIO(image_content)
png_file.name = "test.png"

# Prepare Jpeg file
with open("./examples/test.jpg", "rb") as f:
image_content = f.read()
jpeg_file = BytesIO(image_content)
jpeg_file.name = "test.jpg"

# Upload both files
upload_results: List[UploadResult] = await api.upload_files(
[png_file, jpeg_file], acl="public-read"
)

print("Upload results:")
for result in upload_results:
print(f"- {result.name}: {result.file_key}")
Comment on lines +47 to +54
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for concurrent file uploads.

The concurrent upload implementation looks good, but consider adding error handling for individual file failures.

     # Upload both files
-    upload_results: List[UploadResult] = await api.upload_files(
-        [png_file, jpeg_file], acl="public-read"
-    )
+    try:
+        upload_results: List[UploadResult] = await api.upload_files(
+            [png_file, jpeg_file], acl="public-read"
+        )
+    except Exception as e:
+        print(f"Error uploading files: {e}")
+        return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Upload both files
upload_results: List[UploadResult] = await api.upload_files(
[png_file, jpeg_file], acl="public-read"
)
print("Upload results:")
for result in upload_results:
print(f"- {result.name}: {result.file_key}")
# Upload both files
try:
upload_results: List[UploadResult] = await api.upload_files(
[png_file, jpeg_file], acl="public-read"
)
except Exception as e:
print(f"Error uploading files: {e}")
return
print("Upload results:")
for result in upload_results:
print(f"- {result.name}: {result.file_key}")

print()

# Delete the uploaded files
print("🗑️ Deleting test files...")
file_keys = [result.file_key for result in upload_results]
delete_result = await api.delete_files(file_keys)
print(f"Deleted {delete_result.deleted_count} file(s)")
print(f"Success: {delete_result.success}\n")


if __name__ == "__main__":
asyncio.run(main())
40 changes: 28 additions & 12 deletions examples/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from io import BytesIO
from typing import List

from upyloadthing.client import UTApi
from upyloadthing.schemas import UploadResult
Expand Down Expand Up @@ -27,20 +28,35 @@ def main():
print(file)
print()

# Upload an image file
print("📤 Uploading test image...")
# Prepare test files
print("📤 Uploading test images...")

# Prepare PNG file
with open("./examples/test.png", "rb") as f:
image_content = f.read()
image_file = BytesIO(image_content)
image_file.name = "test.png"
upload_result: UploadResult = api.upload_files(
image_file, acl="public-read"
) # type: ignore
print(f"File uploaded with result: {upload_result}\n")

# Delete the uploaded file
print("🗑️ Deleting test file...")
delete_result = api.delete_files(upload_result.file_key)
png_file = BytesIO(image_content)
png_file.name = "test.png"

# Prepare Jpeg file
with open("./examples/test.jpg", "rb") as f:
image_content = f.read()
jpeg_file = BytesIO(image_content)
jpeg_file.name = "test.jpg"

# Upload both files
upload_results: List[UploadResult] = api.upload_files(
[png_file, jpeg_file], acl="public-read"
)

print("Upload results:")
for result in upload_results:
print(f"- {result.name}: {result.file_key}")
print()

# Delete the uploaded files
print("🗑️ Deleting test files...")
file_keys = [result.file_key for result in upload_results]
delete_result = api.delete_files(file_keys)
print(f"Deleted {delete_result.deleted_count} file(s)")
print(f"Success: {delete_result.success}\n")

Expand Down
Binary file added examples/test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 20 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pyright = "^1.1.393"
ptpython = "^3.0.29"
pytest = "^8.3.4"
respx = "^0.22.0"
pytest-asyncio = "^0.23.2"

[tool.ruff]
line-length = 79
Expand Down
Loading