web3-agent-kit 0.9.0__tar.gz → 1.0.0__tar.gz

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.
Files changed (80) hide show
  1. {web3_agent_kit-0.9.0/web3_agent_kit.egg-info → web3_agent_kit-1.0.0}/PKG-INFO +1 -1
  2. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/pyproject.toml +1 -1
  3. web3_agent_kit-1.0.0/src/airdrop/executor/__init__.py +92 -0
  4. web3_agent_kit-1.0.0/src/airdrop/executor/base_executor.py +413 -0
  5. web3_agent_kit-1.0.0/src/airdrop/executor/captcha_solver.py +634 -0
  6. web3_agent_kit-1.0.0/src/airdrop/executor/galxe_exec.py +554 -0
  7. web3_agent_kit-1.0.0/src/airdrop/executor/intract_exec.py +320 -0
  8. web3_agent_kit-1.0.0/src/airdrop/executor/layer3_exec.py +404 -0
  9. web3_agent_kit-1.0.0/src/airdrop/executor/plugin_registry.py +318 -0
  10. web3_agent_kit-1.0.0/src/airdrop/executor/port3_exec.py +269 -0
  11. web3_agent_kit-1.0.0/src/airdrop/executor/questn.py +344 -0
  12. web3_agent_kit-1.0.0/src/airdrop/executor/taskon.py +241 -0
  13. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/__init__.py +3 -3
  14. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_api.py +1 -1
  15. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_executor.py +9 -0
  16. web3_agent_kit-1.0.0/tests/test_platforms.py +1034 -0
  17. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0/web3_agent_kit.egg-info}/PKG-INFO +1 -1
  18. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/web3_agent_kit.egg-info/SOURCES.txt +10 -0
  19. web3_agent_kit-0.9.0/src/airdrop/executor/__init__.py +0 -33
  20. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/LICENSE +0 -0
  21. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/README.md +0 -0
  22. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/setup.cfg +0 -0
  23. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/setup.py +0 -0
  24. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/__init__.py +0 -0
  25. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/agent.py +0 -0
  26. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/__init__.py +0 -0
  27. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/base.py +0 -0
  28. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/browser.py +0 -0
  29. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/cli.py +0 -0
  30. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/gleam_exec.py +0 -0
  31. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/social_exec.py +0 -0
  32. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/zealy_exec.py +0 -0
  33. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/galxe.py +0 -0
  34. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/gleam.py +0 -0
  35. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/multi_wallet.py +0 -0
  36. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/social.py +0 -0
  37. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/tracker.py +0 -0
  38. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/zealy.py +0 -0
  39. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/models.py +0 -0
  40. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/__init__.py +0 -0
  41. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/approval.py +0 -0
  42. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/bridge.py +0 -0
  43. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/dca.py +0 -0
  44. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/gas.py +0 -0
  45. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/portfolio.py +0 -0
  46. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/swap.py +0 -0
  47. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/wallet.py +0 -0
  48. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/watcher.py +0 -0
  49. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/yield_opt.py +0 -0
  50. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/approval_manager.py +0 -0
  51. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/bridge.py +0 -0
  52. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/chain.py +0 -0
  53. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/dca_bot.py +0 -0
  54. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/defi/__init__.py +0 -0
  55. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/gas_optimizer.py +0 -0
  56. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/llm.py +0 -0
  57. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/multi_wallet.py +0 -0
  58. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/plugins/__init__.py +0 -0
  59. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/plugins/examples/gas_tracker.py +0 -0
  60. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/portfolio.py +0 -0
  61. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/sniper.py +0 -0
  62. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/utils/__init__.py +0 -0
  63. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/wallet.py +0 -0
  64. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/wallet_watcher.py +0 -0
  65. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/yield_optimizer.py +0 -0
  66. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_airdrop.py +0 -0
  67. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_bridge.py +0 -0
  68. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_core.py +0 -0
  69. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_dca_bot.py +0 -0
  70. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_defi.py +0 -0
  71. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_llm.py +0 -0
  72. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_multi_wallet.py +0 -0
  73. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_new_features.py +0 -0
  74. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_plugins.py +0 -0
  75. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_portfolio.py +0 -0
  76. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_sniper.py +0 -0
  77. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_yield_optimizer.py +0 -0
  78. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/web3_agent_kit.egg-info/dependency_links.txt +0 -0
  79. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/web3_agent_kit.egg-info/requires.txt +0 -0
  80. {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/web3_agent_kit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: web3-agent-kit
3
- Version: 0.9.0
3
+ Version: 1.0.0
4
4
  Summary: Open-source framework for building autonomous AI agents that interact with blockchain networks
5
5
  Author-email: Maulana <khasbim240803@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "web3-agent-kit"
7
- version = "0.9.0"
7
+ version = "1.0.0"
8
8
  description = "Open-source framework for building autonomous AI agents that interact with blockchain networks"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,92 @@
1
+ """Executor layer — browser automation for airdrop task completion.
2
+
3
+ Provides real browser-based automation for Gleam.io, Zealy, and social
4
+ platform tasks using Playwright with anti-detect capabilities.
5
+ """
6
+
7
+ from .browser import BrowserManager, BrowserConfig
8
+ from .gleam_exec import GleamExecutor, GleamResult, GleamTaskEntry
9
+ from .social_exec import (
10
+ TwitterExecutor,
11
+ DiscordExecutor,
12
+ TelegramExecutor,
13
+ SocialExecutorConfig,
14
+ )
15
+ from .zealy_exec import ZealyExecutor, ZealyResult
16
+
17
+ # New platform executors
18
+ from .base_executor import (
19
+ BasePlatformExecutor,
20
+ ExecutorConfig,
21
+ ExecutorResult,
22
+ PlatformTask,
23
+ TaskDifficulty,
24
+ )
25
+ from .captcha_solver import (
26
+ CaptchaSolver,
27
+ CaptchaConfig,
28
+ CaptchaProvider,
29
+ CaptchaSolvingError,
30
+ )
31
+ from .questn import QuestNExecutor, QuestNTask, QuestNResult
32
+ from .taskon import TaskOnExecutor, TaskOnTask, TaskOnResult
33
+ from .intract_exec import IntractExecutor, IntractTask, IntractResult
34
+ from .port3_exec import Port3Executor, Port3Task, Port3Result
35
+ from .galxe_exec import GalxeExecutor, GalxeTask, GalxeResult
36
+ from .layer3_exec import Layer3Executor, Layer3Task, Layer3Result
37
+ from .plugin_registry import PlatformPluginRegistry
38
+
39
+ __all__ = [
40
+ # Browser
41
+ "BrowserManager",
42
+ "BrowserConfig",
43
+ # Gleam
44
+ "GleamExecutor",
45
+ "GleamResult",
46
+ "GleamTaskEntry",
47
+ # Social
48
+ "TwitterExecutor",
49
+ "DiscordExecutor",
50
+ "TelegramExecutor",
51
+ "SocialExecutorConfig",
52
+ # Zealy
53
+ "ZealyExecutor",
54
+ "ZealyResult",
55
+ # Base executor
56
+ "BasePlatformExecutor",
57
+ "ExecutorConfig",
58
+ "ExecutorResult",
59
+ "PlatformTask",
60
+ "TaskDifficulty",
61
+ # CAPTCHA
62
+ "CaptchaSolver",
63
+ "CaptchaConfig",
64
+ "CaptchaProvider",
65
+ "CaptchaSolvingError",
66
+ # QuestN
67
+ "QuestNExecutor",
68
+ "QuestNTask",
69
+ "QuestNResult",
70
+ # TaskOn
71
+ "TaskOnExecutor",
72
+ "TaskOnTask",
73
+ "TaskOnResult",
74
+ # Intract
75
+ "IntractExecutor",
76
+ "IntractTask",
77
+ "IntractResult",
78
+ # Port3
79
+ "Port3Executor",
80
+ "Port3Task",
81
+ "Port3Result",
82
+ # Galxe
83
+ "GalxeExecutor",
84
+ "GalxeTask",
85
+ "GalxeResult",
86
+ # Layer3
87
+ "Layer3Executor",
88
+ "Layer3Task",
89
+ "Layer3Result",
90
+ # Plugin Registry
91
+ "PlatformPluginRegistry",
92
+ ]
@@ -0,0 +1,413 @@
1
+ """Base platform executor — abstract class for all airdrop platform executors.
2
+
3
+ Provides a common interface with built-in rate limiting, retry logic,
4
+ logging, and progress tracking. All platform executors extend this class.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import time
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+ from typing import Any, Optional
15
+
16
+ import requests
17
+
18
+ from ..base import AirdropTask, TaskType, TaskStatus
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class TaskDifficulty(Enum):
24
+ """Difficulty level of a platform task."""
25
+ EASY = "easy"
26
+ MEDIUM = "medium"
27
+ HARD = "hard"
28
+
29
+
30
+ @dataclass
31
+ class PlatformTask:
32
+ """A task discovered on a platform."""
33
+ task_id: str
34
+ title: str
35
+ description: str = ""
36
+ task_type: str = "custom"
37
+ url: str = ""
38
+ points: int = 0
39
+ is_completed: bool = False
40
+ is_claimable: bool = False
41
+ difficulty: TaskDifficulty = TaskDifficulty.EASY
42
+ metadata: dict = field(default_factory=dict)
43
+
44
+ def to_airdrop_task(self, platform: str) -> AirdropTask:
45
+ """Convert to an AirdropTask."""
46
+ type_map = {
47
+ "twitter_follow": TaskType.SOCIAL_TWITTER_FOLLOW,
48
+ "twitter_retweet": TaskType.SOCIAL_TWITTER_RETWEET,
49
+ "twitter_like": TaskType.SOCIAL_TWITTER_LIKE,
50
+ "twitter_comment": TaskType.SOCIAL_TWITTER_COMMENT,
51
+ "discord_join": TaskType.SOCIAL_DISCORD_JOIN,
52
+ "discord_verify": TaskType.SOCIAL_DISCORD_VERIFY,
53
+ "telegram_join": TaskType.SOCIAL_TELEGRAM_JOIN,
54
+ "youtube_subscribe": TaskType.SOCIAL_YOUTUBE_SUBSCRIBE,
55
+ "github_star": TaskType.SOCIAL_GITHUB_STAR,
56
+ "github_fork": TaskType.SOCIAL_GITHUB_FORK,
57
+ "on_chain_tx": TaskType.ON_CHAIN_TX,
58
+ "on_chain_swap": TaskType.ON_CHAIN_SWAP,
59
+ "on_chain_bridge": TaskType.ON_CHAIN_BRIDGE,
60
+ "on_chain_stake": TaskType.ON_CHAIN_STAKE,
61
+ "quiz": TaskType.QUIZ,
62
+ "visit_url": TaskType.VISIT_URL,
63
+ "wallet_connect": TaskType.WALLET_CONNECT,
64
+ "referral": TaskType.REFERRAL,
65
+ }
66
+ return AirdropTask(
67
+ task_id=f"{platform}_{self.task_id}",
68
+ platform=platform,
69
+ task_type=type_map.get(self.task_type, TaskType.CUSTOM),
70
+ title=self.title,
71
+ description=self.description,
72
+ url=self.url,
73
+ points=self.points,
74
+ status=TaskStatus.COMPLETED if self.is_completed else TaskStatus.PENDING,
75
+ metadata=self.metadata,
76
+ )
77
+
78
+
79
+ @dataclass
80
+ class ExecutorResult:
81
+ """Result of running an executor."""
82
+ platform: str
83
+ url: str
84
+ total_tasks: int = 0
85
+ completed_tasks: int = 0
86
+ failed_tasks: int = 0
87
+ skipped_tasks: int = 0
88
+ points_earned: int = 0
89
+ tasks: list[PlatformTask] = field(default_factory=list)
90
+ errors: list[str] = field(default_factory=list)
91
+ elapsed_seconds: float = 0.0
92
+
93
+ @property
94
+ def success_rate(self) -> float:
95
+ """Calculate success rate."""
96
+ if self.total_tasks == 0:
97
+ return 0.0
98
+ return self.completed_tasks / self.total_tasks
99
+
100
+ @property
101
+ def is_fully_completed(self) -> bool:
102
+ """Check if all tasks are completed."""
103
+ return self.completed_tasks == self.total_tasks and self.total_tasks > 0
104
+
105
+
106
+ @dataclass
107
+ class ExecutorConfig:
108
+ """Configuration for a platform executor."""
109
+ rate_limit_delay: float = 2.0
110
+ max_retries: int = 3
111
+ retry_delay: float = 5.0
112
+ timeout: int = 30
113
+ proxy: Optional[str] = None
114
+ user_agent: str = (
115
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
116
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
117
+ "Chrome/120.0.0.0 Safari/537.36"
118
+ )
119
+ captcha_api_key: Optional[str] = None
120
+ captcha_provider: str = "anticaptcha"
121
+ verbose: bool = False
122
+
123
+
124
+ class BasePlatformExecutor(ABC):
125
+ """Abstract base class for all platform executors.
126
+
127
+ Provides common functionality: session management, rate limiting,
128
+ retry logic, logging, and progress tracking.
129
+
130
+ Subclasses must implement:
131
+ - visit(url): Navigate to the platform
132
+ - get_tasks(): Discover available tasks
133
+ - complete_task(task): Execute a single task
134
+
135
+ Optional overrides:
136
+ - complete_all(): Complete all tasks (has default implementation)
137
+ - verify(): Verify task completion
138
+ - get_results(): Return results
139
+ - login(credentials): Authenticate
140
+
141
+ Example::
142
+
143
+ class MyPlatform(BasePlatformExecutor):
144
+ platform_name = "my_platform"
145
+
146
+ def visit(self, url):
147
+ self._url = url
148
+ return True
149
+
150
+ def get_tasks(self):
151
+ return [PlatformTask(task_id="1", title="Do something")]
152
+
153
+ def complete_task(self, task):
154
+ return True
155
+ """
156
+
157
+ platform_name: str = "unknown"
158
+ platform_url: str = ""
159
+ supported_task_types: list[str] = []
160
+
161
+ def __init__(self, config: Optional[ExecutorConfig] = None):
162
+ """Initialize the executor.
163
+
164
+ Args:
165
+ config: Optional executor configuration.
166
+ """
167
+ self.config = config or ExecutorConfig()
168
+ self.session = requests.Session()
169
+ self.session.headers.update({"User-Agent": self.config.user_agent})
170
+ if self.config.proxy:
171
+ self.session.proxies = {
172
+ "http": self.config.proxy,
173
+ "https": self.config.proxy,
174
+ }
175
+ self._last_request_time: float = 0
176
+ self._current_url: str = ""
177
+ self._tasks: list[PlatformTask] = []
178
+ self._results: ExecutorResult = ExecutorResult(platform=self.platform_name, url="")
179
+ self._progress_callback: Optional[Any] = None
180
+ logger.info(f"Initialized {self.platform_name} executor")
181
+
182
+ @abstractmethod
183
+ def visit(self, url: str) -> bool:
184
+ """Navigate to the platform URL.
185
+
186
+ Args:
187
+ url: The platform or campaign URL.
188
+
189
+ Returns:
190
+ True if navigation succeeded.
191
+ """
192
+ ...
193
+
194
+ @abstractmethod
195
+ def get_tasks(self) -> list[PlatformTask]:
196
+ """Discover available tasks on the platform.
197
+
198
+ Returns:
199
+ List of PlatformTask objects.
200
+ """
201
+ ...
202
+
203
+ @abstractmethod
204
+ def complete_task(self, task: PlatformTask) -> bool:
205
+ """Execute a single task.
206
+
207
+ Args:
208
+ task: The task to complete.
209
+
210
+ Returns:
211
+ True if task was completed successfully.
212
+ """
213
+ ...
214
+
215
+ def complete_all(
216
+ self,
217
+ url: str,
218
+ tracker: Optional[Any] = None,
219
+ ) -> ExecutorResult:
220
+ """Complete all tasks at the given URL.
221
+
222
+ Default implementation: visit → get_tasks → complete each task.
223
+
224
+ Args:
225
+ url: Platform or campaign URL.
226
+ tracker: Optional AirdropTracker for progress tracking.
227
+
228
+ Returns:
229
+ ExecutorResult with completion statistics.
230
+ """
231
+ start_time = time.time()
232
+ self._results = ExecutorResult(platform=self.platform_name, url=url)
233
+
234
+ try:
235
+ # Visit the platform
236
+ if not self.visit(url):
237
+ self._results.errors.append(f"Failed to load {url}")
238
+ return self._results
239
+
240
+ # Get available tasks
241
+ tasks = self.get_tasks()
242
+ self._results.total_tasks = len(tasks)
243
+ self._results.tasks = tasks
244
+
245
+ if not tasks:
246
+ self._results.errors.append("No tasks found")
247
+ return self._results
248
+
249
+ logger.info(f"{self.platform_name}: starting {len(tasks)} tasks")
250
+
251
+ # Complete each task
252
+ for i, task in enumerate(tasks):
253
+ if task.is_completed:
254
+ self._results.skipped_tasks += 1
255
+ continue
256
+
257
+ logger.info(
258
+ f"{self.platform_name}: [{i + 1}/{len(tasks)}] {task.title}"
259
+ )
260
+
261
+ # Notify progress callback
262
+ if self._progress_callback:
263
+ self._progress_callback(i, len(tasks), task.title)
264
+
265
+ success = self.complete_task(task)
266
+ if success:
267
+ self._results.completed_tasks += 1
268
+ self._results.points_earned += task.points
269
+ task.is_completed = True
270
+
271
+ if tracker:
272
+ airdrop_task = task.to_airdrop_task(self.platform_name)
273
+ tracker.mark_task_completed(airdrop_task)
274
+ else:
275
+ self._results.failed_tasks += 1
276
+ self._results.errors.append(f"Failed: {task.title}")
277
+
278
+ # Rate limiting between tasks
279
+ self._rate_limit()
280
+
281
+ except Exception as e:
282
+ self._results.errors.append(f"Unexpected error: {str(e)}")
283
+ logger.error(f"{self.platform_name}: unexpected error: {e}")
284
+ finally:
285
+ self._results.elapsed_seconds = time.time() - start_time
286
+ logger.info(
287
+ f"{self.platform_name}: finished in {self._results.elapsed_seconds:.1f}s — "
288
+ f"{self._results.completed_tasks}/{self._results.total_tasks} completed"
289
+ )
290
+
291
+ return self._results
292
+
293
+ def verify(self) -> bool:
294
+ """Verify completion status. Override in subclasses.
295
+
296
+ Returns:
297
+ True if verification succeeded.
298
+ """
299
+ return True
300
+
301
+ def get_results(self) -> ExecutorResult:
302
+ """Get the current results.
303
+
304
+ Returns:
305
+ ExecutorResult object.
306
+ """
307
+ return self._results
308
+
309
+ def login(self, credentials: dict) -> bool:
310
+ """Authenticate with the platform. Override if needed.
311
+
312
+ Args:
313
+ credentials: Platform-specific credentials.
314
+
315
+ Returns:
316
+ True if login succeeded.
317
+ """
318
+ logger.info(f"{self.platform_name}: login not required")
319
+ return True
320
+
321
+ def set_progress_callback(self, callback: Any) -> None:
322
+ """Set a progress callback function.
323
+
324
+ Args:
325
+ callback: Function(current, total, task_name) called on progress.
326
+ """
327
+ self._progress_callback = callback
328
+
329
+ def close(self) -> None:
330
+ """Clean up resources."""
331
+ self.session.close()
332
+ logger.debug(f"{self.platform_name}: closed")
333
+
334
+ # ─── Helper Methods ──────────────────────────────────────────
335
+
336
+ def _rate_limit(self) -> None:
337
+ """Enforce rate limiting between requests."""
338
+ elapsed = time.time() - self._last_request_time
339
+ if elapsed < self.config.rate_limit_delay:
340
+ sleep_time = self.config.rate_limit_delay - elapsed
341
+ logger.debug(f"Rate limiting: sleeping {sleep_time:.1f}s")
342
+ time.sleep(sleep_time)
343
+ self._last_request_time = time.time()
344
+
345
+ def _request(
346
+ self,
347
+ method: str,
348
+ url: str,
349
+ **kwargs,
350
+ ) -> requests.Response:
351
+ """Make an HTTP request with rate limiting and retry logic.
352
+
353
+ Args:
354
+ method: HTTP method (GET, POST, etc.).
355
+ url: Request URL.
356
+ **kwargs: Passed to requests.
357
+
358
+ Returns:
359
+ Response object.
360
+
361
+ Raises:
362
+ requests.RequestException: After all retries exhausted.
363
+ """
364
+ kwargs.setdefault("timeout", self.config.timeout)
365
+ last_error = None
366
+
367
+ for attempt in range(self.config.max_retries):
368
+ self._rate_limit()
369
+ try:
370
+ response = self.session.request(method, url, **kwargs)
371
+ if response.status_code == 429:
372
+ retry_after = int(
373
+ response.headers.get("Retry-After", self.config.retry_delay)
374
+ )
375
+ logger.warning(f"Rate limited, waiting {retry_after}s")
376
+ time.sleep(retry_after)
377
+ continue
378
+ response.raise_for_status()
379
+ return response
380
+ except requests.RequestException as e:
381
+ last_error = e
382
+ logger.warning(f"Request failed (attempt {attempt + 1}): {e}")
383
+ if attempt < self.config.max_retries - 1:
384
+ time.sleep(self.config.retry_delay * (attempt + 1))
385
+
386
+ raise last_error # type: ignore[misc]
387
+
388
+ def _get(self, url: str, **kwargs) -> requests.Response:
389
+ """Make a GET request."""
390
+ return self._request("GET", url, **kwargs)
391
+
392
+ def _post(self, url: str, **kwargs) -> requests.Response:
393
+ """Make a POST request."""
394
+ return self._request("POST", url, **kwargs)
395
+
396
+ def _extract_id_from_url(self, url: str) -> str:
397
+ """Extract an ID from a URL. Override for platform-specific parsing."""
398
+ import re
399
+ # Try to find a UUID or alphanumeric ID in the URL
400
+ match = re.search(r"[0-9a-f]{24}", url)
401
+ if match:
402
+ return match.group(0)
403
+ # Fallback: use last path segment
404
+ from urllib.parse import urlparse
405
+ parsed = urlparse(url)
406
+ path_parts = [p for p in parsed.path.split("/") if p]
407
+ return path_parts[-1] if path_parts else "unknown"
408
+
409
+ def __enter__(self):
410
+ return self
411
+
412
+ def __exit__(self, *args):
413
+ self.close()