sleap-share 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sleap_share/__init__.py +302 -0
- sleap_share/auth.py +330 -0
- sleap_share/cli.py +462 -0
- sleap_share/client.py +677 -0
- sleap_share/config.py +103 -0
- sleap_share/exceptions.py +127 -0
- sleap_share/models.py +293 -0
- sleap_share-0.1.2.dist-info/METADATA +204 -0
- sleap_share-0.1.2.dist-info/RECORD +11 -0
- sleap_share-0.1.2.dist-info/WHEEL +4 -0
- sleap_share-0.1.2.dist-info/entry_points.txt +2 -0
sleap_share/config.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Configuration and constants for sleap-share client."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from platformdirs import user_config_dir
|
|
9
|
+
|
|
10
|
+
# Environment type
|
|
11
|
+
Environment = Literal["production", "staging"]
|
|
12
|
+
|
|
13
|
+
# Base URLs for each environment
|
|
14
|
+
URLS = {
|
|
15
|
+
"production": "https://slp.sh",
|
|
16
|
+
"staging": "https://staging.slp.sh",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Default environment
|
|
20
|
+
DEFAULT_ENV: Environment = "production"
|
|
21
|
+
|
|
22
|
+
# Environment variable for overriding default environment
|
|
23
|
+
ENV_VAR = "SLEAP_SHARE_ENV"
|
|
24
|
+
|
|
25
|
+
# App name for config/credential paths
|
|
26
|
+
APP_NAME = "sleap-share"
|
|
27
|
+
|
|
28
|
+
# Keyring service name
|
|
29
|
+
KEYRING_SERVICE = "sleap-share"
|
|
30
|
+
KEYRING_USERNAME = "api_token"
|
|
31
|
+
|
|
32
|
+
# Credentials file name (fallback when keyring unavailable)
|
|
33
|
+
CREDENTIALS_FILE = "credentials"
|
|
34
|
+
|
|
35
|
+
# HTTP client settings
|
|
36
|
+
DEFAULT_TIMEOUT = 30.0 # seconds
|
|
37
|
+
UPLOAD_TIMEOUT = 600.0 # 10 minutes for large uploads
|
|
38
|
+
DOWNLOAD_CHUNK_SIZE = 8192 # 8KB chunks for streaming
|
|
39
|
+
|
|
40
|
+
# Allowed file extensions
|
|
41
|
+
ALLOWED_EXTENSIONS = {".slp"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Config:
|
|
46
|
+
"""Runtime configuration for the sleap-share client."""
|
|
47
|
+
|
|
48
|
+
env: Environment = DEFAULT_ENV
|
|
49
|
+
base_url: str | None = None # If set, overrides env-based URL
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def url(self) -> str:
|
|
53
|
+
"""Get the base URL for the current environment."""
|
|
54
|
+
if self.base_url:
|
|
55
|
+
return self.base_url.rstrip("/")
|
|
56
|
+
return URLS[self.env]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def config_dir(self) -> Path:
|
|
60
|
+
"""Get the configuration directory path."""
|
|
61
|
+
return Path(user_config_dir(APP_NAME))
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def credentials_path(self) -> Path:
|
|
65
|
+
"""Get the path to the credentials file (fallback storage)."""
|
|
66
|
+
return self.config_dir / CREDENTIALS_FILE
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_env_from_environment() -> Environment:
|
|
70
|
+
"""Get environment from environment variable, or default."""
|
|
71
|
+
env_value = os.environ.get(ENV_VAR, "").lower()
|
|
72
|
+
if env_value in ("staging", "stg"):
|
|
73
|
+
return "staging"
|
|
74
|
+
return DEFAULT_ENV
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validate_env(env: str | None) -> Environment | None:
|
|
78
|
+
"""Validate and convert env string to Environment type."""
|
|
79
|
+
if env is None:
|
|
80
|
+
return None
|
|
81
|
+
if env in ("production", "staging"):
|
|
82
|
+
return env # type: ignore[return-value]
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_config(
|
|
87
|
+
env: str | None = None,
|
|
88
|
+
base_url: str | None = None,
|
|
89
|
+
) -> Config:
|
|
90
|
+
"""Create a configuration object.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
env: Target environment (production or staging).
|
|
94
|
+
If not specified, uses SLEAP_SHARE_ENV or defaults to production.
|
|
95
|
+
base_url: Override base URL directly (ignores env setting).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Config object with the resolved settings.
|
|
99
|
+
"""
|
|
100
|
+
validated_env = _validate_env(env)
|
|
101
|
+
if validated_env is None:
|
|
102
|
+
validated_env = get_env_from_environment()
|
|
103
|
+
return Config(env=validated_env, base_url=base_url)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Custom exceptions for sleap-share client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SleapShareError(Exception):
|
|
7
|
+
"""Base exception for sleap-share client errors.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
message: Human-readable error message.
|
|
11
|
+
code: Machine-readable error code (e.g., "not_found", "rate_limited").
|
|
12
|
+
status_code: HTTP status code if applicable.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
code: str = "unknown_error",
|
|
19
|
+
status_code: int | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.message = message
|
|
23
|
+
self.code = code
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return self.message
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AuthenticationError(SleapShareError):
|
|
31
|
+
"""Raised when authentication fails or token is invalid."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
message: str = "Authentication required. Run 'sleap-share login' to authenticate.",
|
|
36
|
+
code: str = "authentication_required",
|
|
37
|
+
status_code: int = 401,
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(message, code, status_code)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NotFoundError(SleapShareError):
|
|
43
|
+
"""Raised when a requested resource is not found."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
message: str = "The requested resource was not found.",
|
|
48
|
+
code: str = "not_found",
|
|
49
|
+
status_code: int = 404,
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(message, code, status_code)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RateLimitError(SleapShareError):
|
|
55
|
+
"""Raised when rate limit is exceeded."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
message: str = "Rate limit exceeded. Please try again later.",
|
|
60
|
+
code: str = "rate_limited",
|
|
61
|
+
status_code: int = 429,
|
|
62
|
+
retry_after: int | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
if retry_after:
|
|
65
|
+
message = f"{message} Retry after {retry_after} seconds."
|
|
66
|
+
super().__init__(message, code, status_code)
|
|
67
|
+
self.retry_after = retry_after
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ValidationError(SleapShareError):
|
|
71
|
+
"""Raised when input validation fails."""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
message: str = "Validation error.",
|
|
76
|
+
code: str = "validation_error",
|
|
77
|
+
status_code: int = 400,
|
|
78
|
+
) -> None:
|
|
79
|
+
super().__init__(message, code, status_code)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PermissionError(SleapShareError):
|
|
83
|
+
"""Raised when the user lacks permission for an operation."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
message: str = "You do not have permission to perform this action.",
|
|
88
|
+
code: str = "permission_denied",
|
|
89
|
+
status_code: int = 403,
|
|
90
|
+
) -> None:
|
|
91
|
+
super().__init__(message, code, status_code)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class UploadError(SleapShareError):
|
|
95
|
+
"""Raised when file upload fails."""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
message: str = "File upload failed.",
|
|
100
|
+
code: str = "upload_failed",
|
|
101
|
+
status_code: int | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
super().__init__(message, code, status_code)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class DownloadError(SleapShareError):
|
|
107
|
+
"""Raised when file download fails."""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
message: str = "File download failed.",
|
|
112
|
+
code: str = "download_failed",
|
|
113
|
+
status_code: int | None = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
super().__init__(message, code, status_code)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class NetworkError(SleapShareError):
|
|
119
|
+
"""Raised when a network error occurs."""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
message: str = "Network error. Please check your internet connection.",
|
|
124
|
+
code: str = "network_error",
|
|
125
|
+
status_code: int | None = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
super().__init__(message, code, status_code)
|
sleap_share/models.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Data models for sleap-share client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _parse_datetime(value: str | int | datetime | None) -> datetime | None:
|
|
11
|
+
"""Parse a datetime from ISO format string, Unix timestamp, or return as-is."""
|
|
12
|
+
if value is None:
|
|
13
|
+
return None
|
|
14
|
+
if isinstance(value, datetime):
|
|
15
|
+
return value
|
|
16
|
+
# Handle Unix timestamp in milliseconds
|
|
17
|
+
if isinstance(value, int):
|
|
18
|
+
try:
|
|
19
|
+
return datetime.fromtimestamp(value / 1000)
|
|
20
|
+
except (ValueError, OSError):
|
|
21
|
+
return None
|
|
22
|
+
# Handle ISO format with or without timezone
|
|
23
|
+
try:
|
|
24
|
+
# Try with timezone (Z suffix)
|
|
25
|
+
if value.endswith("Z"):
|
|
26
|
+
value = value[:-1] + "+00:00"
|
|
27
|
+
return datetime.fromisoformat(value)
|
|
28
|
+
except ValueError:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class URLs:
|
|
34
|
+
"""All URLs for a shortcode.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
share_url: Landing page URL (https://slp.sh/{shortcode}).
|
|
38
|
+
download_url: Direct file download URL (https://slp.sh/{shortcode}/labels.slp).
|
|
39
|
+
metadata_url: Metadata JSON URL (https://slp.sh/{shortcode}/metadata.json).
|
|
40
|
+
preview_url: Preview image URL (https://slp.sh/{shortcode}/preview.png).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
share_url: str
|
|
44
|
+
download_url: str
|
|
45
|
+
metadata_url: str
|
|
46
|
+
preview_url: str
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_shortcode(cls, shortcode: str, base_url: str = "https://slp.sh") -> URLs:
|
|
50
|
+
"""Create URLs from a shortcode.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
shortcode: The file shortcode.
|
|
54
|
+
base_url: Base URL for the environment.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
URLs object with all URLs populated.
|
|
58
|
+
"""
|
|
59
|
+
base = base_url.rstrip("/")
|
|
60
|
+
return cls(
|
|
61
|
+
share_url=f"{base}/{shortcode}",
|
|
62
|
+
download_url=f"{base}/{shortcode}/labels.slp",
|
|
63
|
+
metadata_url=f"{base}/{shortcode}/metadata.json",
|
|
64
|
+
preview_url=f"{base}/{shortcode}/preview.png",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class UploadResult:
|
|
70
|
+
"""Result of a successful file upload.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
shortcode: The unique shortcode for the file.
|
|
74
|
+
share_url: The shareable URL for the file.
|
|
75
|
+
data_url: Direct download URL for the file.
|
|
76
|
+
expires_at: When the file will expire (None if permanent).
|
|
77
|
+
is_permanent: Whether the file is permanently stored.
|
|
78
|
+
validation_status: Status of file validation ("valid", "invalid", "pending").
|
|
79
|
+
metadata: Optional metadata extracted from the file.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
shortcode: str
|
|
83
|
+
share_url: str
|
|
84
|
+
data_url: str
|
|
85
|
+
expires_at: datetime | None
|
|
86
|
+
is_permanent: bool
|
|
87
|
+
validation_status: str
|
|
88
|
+
metadata: Metadata | None = None
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_api_response(cls, data: dict[str, Any], base_url: str) -> UploadResult:
|
|
92
|
+
"""Create from API response data.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
data: Response data from /api/upload/complete endpoint.
|
|
96
|
+
base_url: Base URL for the environment.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
UploadResult object.
|
|
100
|
+
"""
|
|
101
|
+
# Extract shortcode from shareUrl (API returns camelCase)
|
|
102
|
+
share_url = data.get("shareUrl", "")
|
|
103
|
+
shortcode = share_url.rstrip("/").split("/")[-1] if share_url else ""
|
|
104
|
+
|
|
105
|
+
metadata = None
|
|
106
|
+
if "metadata" in data and data["metadata"]:
|
|
107
|
+
# Map camelCase API response to snake_case for Metadata
|
|
108
|
+
meta_data = data["metadata"]
|
|
109
|
+
mapped_metadata = {
|
|
110
|
+
"labeled_frames_count": meta_data.get("labeledFramesCount"),
|
|
111
|
+
"user_instances_count": meta_data.get("userInstancesCount"),
|
|
112
|
+
"predicted_instances_count": meta_data.get("predictedInstancesCount"),
|
|
113
|
+
"tracks_count": meta_data.get("tracksCount"),
|
|
114
|
+
"videos_count": meta_data.get("videosCount"),
|
|
115
|
+
}
|
|
116
|
+
metadata = Metadata.from_dict(mapped_metadata)
|
|
117
|
+
|
|
118
|
+
return cls(
|
|
119
|
+
shortcode=shortcode,
|
|
120
|
+
share_url=share_url,
|
|
121
|
+
data_url=data.get("dataUrl", f"{base_url}/{shortcode}/labels.slp"),
|
|
122
|
+
expires_at=_parse_datetime(data.get("expiresAt")),
|
|
123
|
+
is_permanent=data.get("isPermanent", False),
|
|
124
|
+
validation_status=data.get("validationStatus", "unknown"),
|
|
125
|
+
metadata=metadata,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class FileInfo:
|
|
131
|
+
"""Basic information about an uploaded file.
|
|
132
|
+
|
|
133
|
+
Attributes:
|
|
134
|
+
shortcode: The unique shortcode for the file.
|
|
135
|
+
filename: Original filename.
|
|
136
|
+
file_size: File size in bytes.
|
|
137
|
+
created_at: When the file was uploaded.
|
|
138
|
+
expires_at: When the file will expire (None if permanent).
|
|
139
|
+
share_url: The shareable URL for the file.
|
|
140
|
+
data_url: Direct download URL for the file.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
shortcode: str
|
|
144
|
+
filename: str
|
|
145
|
+
file_size: int
|
|
146
|
+
created_at: datetime
|
|
147
|
+
expires_at: datetime | None
|
|
148
|
+
share_url: str
|
|
149
|
+
data_url: str
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def from_api_response(cls, data: dict[str, Any], base_url: str) -> FileInfo:
|
|
153
|
+
"""Create from API response data.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
data: Response data from file listing API.
|
|
157
|
+
base_url: Base URL for the environment.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
FileInfo object.
|
|
161
|
+
"""
|
|
162
|
+
shortcode = data["shortcode"]
|
|
163
|
+
urls = URLs.from_shortcode(shortcode, base_url)
|
|
164
|
+
|
|
165
|
+
created_at = _parse_datetime(data.get("created_at"))
|
|
166
|
+
if created_at is None:
|
|
167
|
+
created_at = datetime.now()
|
|
168
|
+
|
|
169
|
+
return cls(
|
|
170
|
+
shortcode=shortcode,
|
|
171
|
+
filename=data.get("filename", data.get("original_filename", "unknown")),
|
|
172
|
+
file_size=data.get("file_size", 0),
|
|
173
|
+
created_at=created_at,
|
|
174
|
+
expires_at=_parse_datetime(data.get("expires_at")),
|
|
175
|
+
share_url=urls.share_url,
|
|
176
|
+
data_url=urls.download_url,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class Metadata:
|
|
182
|
+
"""Full metadata from /{shortcode}/metadata.json.
|
|
183
|
+
|
|
184
|
+
Attributes:
|
|
185
|
+
shortcode: The unique shortcode for the file.
|
|
186
|
+
original_filename: Original filename at upload.
|
|
187
|
+
file_size: File size in bytes.
|
|
188
|
+
upload_timestamp: When the file was uploaded.
|
|
189
|
+
expires_at: When the file will expire (None if permanent).
|
|
190
|
+
validation_status: Status of file validation.
|
|
191
|
+
labeled_frames_count: Number of labeled frames (SLP-specific).
|
|
192
|
+
user_instances_count: Number of user instances (SLP-specific).
|
|
193
|
+
predicted_instances_count: Number of predicted instances (SLP-specific).
|
|
194
|
+
tracks_count: Number of tracks (SLP-specific).
|
|
195
|
+
videos_count: Number of videos (SLP-specific).
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
shortcode: str
|
|
199
|
+
original_filename: str
|
|
200
|
+
file_size: int
|
|
201
|
+
upload_timestamp: datetime
|
|
202
|
+
expires_at: datetime | None
|
|
203
|
+
validation_status: str
|
|
204
|
+
# SLP-specific stats (may be None if validation failed)
|
|
205
|
+
labeled_frames_count: int | None = None
|
|
206
|
+
user_instances_count: int | None = None
|
|
207
|
+
predicted_instances_count: int | None = None
|
|
208
|
+
tracks_count: int | None = None
|
|
209
|
+
videos_count: int | None = None
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def from_dict(cls, data: dict[str, Any]) -> Metadata:
|
|
213
|
+
"""Create from metadata dictionary.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
data: Metadata dictionary.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Metadata object.
|
|
220
|
+
"""
|
|
221
|
+
upload_timestamp = _parse_datetime(data.get("upload_timestamp"))
|
|
222
|
+
if upload_timestamp is None:
|
|
223
|
+
upload_timestamp = datetime.now()
|
|
224
|
+
|
|
225
|
+
return cls(
|
|
226
|
+
shortcode=data.get("shortcode", ""),
|
|
227
|
+
original_filename=data.get("original_filename", "unknown"),
|
|
228
|
+
file_size=data.get("file_size", 0),
|
|
229
|
+
upload_timestamp=upload_timestamp,
|
|
230
|
+
expires_at=_parse_datetime(data.get("expires_at")),
|
|
231
|
+
validation_status=data.get("validation_status", "unknown"),
|
|
232
|
+
labeled_frames_count=data.get("labeled_frames_count"),
|
|
233
|
+
user_instances_count=data.get("user_instances_count"),
|
|
234
|
+
predicted_instances_count=data.get("predicted_instances_count"),
|
|
235
|
+
tracks_count=data.get("tracks_count"),
|
|
236
|
+
videos_count=data.get("videos_count"),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def to_dict(self) -> dict[str, Any]:
|
|
240
|
+
"""Convert to dictionary for JSON serialization."""
|
|
241
|
+
return {
|
|
242
|
+
"shortcode": self.shortcode,
|
|
243
|
+
"original_filename": self.original_filename,
|
|
244
|
+
"file_size": self.file_size,
|
|
245
|
+
"upload_timestamp": self.upload_timestamp.isoformat(),
|
|
246
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
247
|
+
"validation_status": self.validation_status,
|
|
248
|
+
"labeled_frames_count": self.labeled_frames_count,
|
|
249
|
+
"user_instances_count": self.user_instances_count,
|
|
250
|
+
"predicted_instances_count": self.predicted_instances_count,
|
|
251
|
+
"tracks_count": self.tracks_count,
|
|
252
|
+
"videos_count": self.videos_count,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass
|
|
257
|
+
class User:
|
|
258
|
+
"""Authenticated user profile.
|
|
259
|
+
|
|
260
|
+
Attributes:
|
|
261
|
+
id: User ID.
|
|
262
|
+
username: GitHub username.
|
|
263
|
+
email: User email.
|
|
264
|
+
avatar_url: URL to user's avatar image.
|
|
265
|
+
total_files: Total number of files uploaded.
|
|
266
|
+
total_storage: Total storage used in bytes.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
id: int
|
|
270
|
+
username: str
|
|
271
|
+
email: str
|
|
272
|
+
avatar_url: str
|
|
273
|
+
total_files: int
|
|
274
|
+
total_storage: int
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def from_api_response(cls, data: dict[str, Any]) -> User:
|
|
278
|
+
"""Create from API response data.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
data: Response data from /api/v1/user/me endpoint.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
User object.
|
|
285
|
+
"""
|
|
286
|
+
return cls(
|
|
287
|
+
id=data.get("id", 0),
|
|
288
|
+
username=data.get("username", ""),
|
|
289
|
+
email=data.get("email", ""),
|
|
290
|
+
avatar_url=data.get("avatar_url", ""),
|
|
291
|
+
total_files=data.get("total_files", 0),
|
|
292
|
+
total_storage=data.get("total_storage", 0),
|
|
293
|
+
)
|