2
0
Files
gitcaddy-server/sdk/python/gitea/models.py
logikonline ca8557f033 feat(sdk): add CLI tool and SDK libraries for developer tooling (Phase 4)
Add comprehensive developer tooling for Gitea integration:

CLI Tool (cmd/gitea-cli/):
- gitea-cli auth login/logout/status - Authentication management
- gitea-cli upload release-asset - Chunked upload with progress
- gitea-cli upload resume - Resume interrupted uploads
- gitea-cli upload list - List pending upload sessions
- Parallel chunk uploads with configurable workers
- SHA256 checksum verification
- Progress bar with speed and ETA display

Go SDK (sdk/go/):
- GiteaClient with token authentication
- User, Repository, Release, Attachment types
- ChunkedUpload with parallel workers
- Progress callbacks for upload tracking
- Functional options pattern (WithChunkSize, WithParallel, etc.)

Python SDK (sdk/python/):
- GiteaClient with requests-based HTTP
- Full type hints and dataclasses
- ThreadPoolExecutor for parallel uploads
- Resume capability for interrupted uploads
- Exception hierarchy (APIError, UploadError, etc.)

TypeScript SDK (sdk/typescript/):
- Full TypeScript types and interfaces
- Async/await API design
- Browser and Node.js compatible
- Web Crypto API for checksums
- ESM and CJS build outputs

All SDKs support:
- Chunked uploads for large files
- Parallel upload workers
- Progress tracking with callbacks
- Checksum verification
- Resume interrupted uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 12:07:07 -05:00

223 lines
6.3 KiB
Python

# Copyright 2026 The Gitea Authors. All rights reserved.
# SPDX-License-Identifier: MIT
"""Data models for the Gitea SDK."""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, List, Dict, Any
@dataclass
class User:
"""Represents a Gitea user."""
id: int
login: str
full_name: str = ""
email: str = ""
avatar_url: str = ""
is_admin: bool = False
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "User":
return cls(
id=data.get("id", 0),
login=data.get("login", ""),
full_name=data.get("full_name", ""),
email=data.get("email", ""),
avatar_url=data.get("avatar_url", ""),
is_admin=data.get("is_admin", False),
)
@dataclass
class Repository:
"""Represents a Gitea repository."""
id: int
name: str
full_name: str
owner: Optional[User] = None
description: str = ""
private: bool = False
fork: bool = False
default_branch: str = "main"
stars_count: int = 0
forks_count: int = 0
clone_url: str = ""
html_url: str = ""
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Repository":
owner = None
if data.get("owner"):
owner = User.from_dict(data["owner"])
return cls(
id=data.get("id", 0),
name=data.get("name", ""),
full_name=data.get("full_name", ""),
owner=owner,
description=data.get("description", ""),
private=data.get("private", False),
fork=data.get("fork", False),
default_branch=data.get("default_branch", "main"),
stars_count=data.get("stars_count", 0),
forks_count=data.get("forks_count", 0),
clone_url=data.get("clone_url", ""),
html_url=data.get("html_url", ""),
)
@dataclass
class Attachment:
"""Represents a release attachment/asset."""
id: int
name: str
size: int
download_count: int = 0
download_url: str = ""
created_at: Optional[datetime] = None
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Attachment":
created_at = None
if data.get("created_at"):
try:
created_at = datetime.fromisoformat(data["created_at"].replace("Z", "+00:00"))
except (ValueError, AttributeError):
pass
return cls(
id=data.get("id", 0),
name=data.get("name", ""),
size=data.get("size", 0),
download_count=data.get("download_count", 0),
download_url=data.get("browser_download_url", ""),
created_at=created_at,
)
@dataclass
class Release:
"""Represents a Gitea release."""
id: int
tag_name: str
name: str = ""
body: str = ""
draft: bool = False
prerelease: bool = False
published_at: Optional[datetime] = None
assets: List[Attachment] = field(default_factory=list)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Release":
published_at = None
if data.get("published_at"):
try:
published_at = datetime.fromisoformat(data["published_at"].replace("Z", "+00:00"))
except (ValueError, AttributeError):
pass
assets = []
for asset_data in data.get("assets", []):
assets.append(Attachment.from_dict(asset_data))
return cls(
id=data.get("id", 0),
tag_name=data.get("tag_name", ""),
name=data.get("name", ""),
body=data.get("body", ""),
draft=data.get("draft", False),
prerelease=data.get("prerelease", False),
published_at=published_at,
assets=assets,
)
@dataclass
class UploadSession:
"""Represents a chunked upload session."""
id: str
file_name: str
file_size: int
chunk_size: int
total_chunks: int
chunks_received: int = 0
status: str = "pending"
expires_at: Optional[datetime] = None
checksum: str = ""
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "UploadSession":
expires_at = None
if data.get("expires_at"):
try:
expires_at = datetime.fromisoformat(data["expires_at"].replace("Z", "+00:00"))
except (ValueError, AttributeError):
pass
return cls(
id=data.get("id", ""),
file_name=data.get("file_name", ""),
file_size=data.get("file_size", 0),
chunk_size=data.get("chunk_size", 0),
total_chunks=data.get("total_chunks", 0),
chunks_received=data.get("chunks_received", 0),
status=data.get("status", "pending"),
expires_at=expires_at,
checksum=data.get("checksum", ""),
)
@dataclass
class UploadResult:
"""Represents the result of a completed upload."""
id: int
name: str
size: int
download_url: str
checksum_verified: bool = False
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "UploadResult":
return cls(
id=data.get("id", 0),
name=data.get("name", ""),
size=data.get("size", 0),
download_url=data.get("browser_download_url", ""),
checksum_verified=data.get("checksum_verified", False),
)
@dataclass
class Progress:
"""Represents upload progress."""
bytes_done: int
bytes_total: int
chunks_done: int
chunks_total: int
percent: float
speed: float # bytes per second
eta_seconds: float
@property
def eta(self) -> str:
"""Format ETA as a human-readable string."""
seconds = int(self.eta_seconds)
if seconds < 60:
return f"{seconds}s"
minutes = seconds // 60
seconds = seconds % 60
if minutes < 60:
return f"{minutes}m{seconds}s"
hours = minutes // 60
minutes = minutes % 60
return f"{hours}h{minutes}m"
@property
def speed_formatted(self) -> str:
"""Format speed as a human-readable string."""
if self.speed < 1024:
return f"{self.speed:.0f} B/s"
elif self.speed < 1024 * 1024:
return f"{self.speed / 1024:.1f} KB/s"
else:
return f"{self.speed / 1024 / 1024:.1f} MB/s"