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>
223 lines
6.3 KiB
Python
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"
|