webscout 2025.10.16__py3-none-any.whl → 2025.10.17__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.
Potentially problematic release.
This version of webscout might be problematic. Click here for more details.
- webscout/Provider/ClaudeOnline.py +350 -0
- webscout/Provider/TTI/claudeonline.py +315 -0
- webscout/version.py +1 -1
- webscout/version.py.bak +1 -1
- {webscout-2025.10.16.dist-info → webscout-2025.10.17.dist-info}/METADATA +2 -3
- {webscout-2025.10.16.dist-info → webscout-2025.10.17.dist-info}/RECORD +10 -9
- webscout/Extra/weather.md +0 -281
- {webscout-2025.10.16.dist-info → webscout-2025.10.17.dist-info}/WHEEL +0 -0
- {webscout-2025.10.16.dist-info → webscout-2025.10.17.dist-info}/entry_points.txt +0 -0
- {webscout-2025.10.16.dist-info → webscout-2025.10.17.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-2025.10.16.dist-info → webscout-2025.10.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from typing import Any, Dict, Generator, Optional, Union
|
|
2
|
+
|
|
3
|
+
from curl_cffi import CurlError
|
|
4
|
+
from curl_cffi.requests import Session
|
|
5
|
+
|
|
6
|
+
from webscout import exceptions
|
|
7
|
+
from webscout.AIbase import Provider
|
|
8
|
+
from webscout.AIutel import AwesomePrompts, Conversation, Optimizers
|
|
9
|
+
from webscout.litagent import LitAgent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClaudeOnline(Provider):
|
|
13
|
+
"""
|
|
14
|
+
A class to interact with the Claude Online API (claude.online/chat).
|
|
15
|
+
|
|
16
|
+
This provider implements the reverse-engineered API from claude.online,
|
|
17
|
+
providing access to Claude AI through their web interface backend.
|
|
18
|
+
"""
|
|
19
|
+
required_auth = False
|
|
20
|
+
AVAILABLE_MODELS = ["claude-online"]
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
is_conversation: bool = True,
|
|
25
|
+
max_tokens: int = 2049,
|
|
26
|
+
timeout: int = 30,
|
|
27
|
+
intro: str = None,
|
|
28
|
+
filepath: str = None,
|
|
29
|
+
update_file: bool = True,
|
|
30
|
+
proxies: dict = {},
|
|
31
|
+
history_offset: int = 10250,
|
|
32
|
+
act: str = None,
|
|
33
|
+
system_prompt: str = "You are a helpful assistant.",
|
|
34
|
+
model: str = "claude-online"
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initializes the Claude Online API client.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
is_conversation: Whether the provider is in conversation mode.
|
|
41
|
+
max_tokens: Maximum number of tokens to sample.
|
|
42
|
+
timeout: Timeout for API requests.
|
|
43
|
+
intro: Introduction message for the conversation.
|
|
44
|
+
filepath: Filepath for storing conversation history.
|
|
45
|
+
update_file: Whether to update the conversation history file.
|
|
46
|
+
proxies: Proxies for the API requests.
|
|
47
|
+
history_offset: Offset for conversation history.
|
|
48
|
+
act: Act for the conversation.
|
|
49
|
+
system_prompt: The system prompt to define the assistant's role.
|
|
50
|
+
model: Model to use (only "claude-online" supported).
|
|
51
|
+
"""
|
|
52
|
+
if model not in self.AVAILABLE_MODELS:
|
|
53
|
+
raise ValueError(f"Invalid model: {model}. Choose from: {self.AVAILABLE_MODELS}")
|
|
54
|
+
|
|
55
|
+
# API endpoints
|
|
56
|
+
self.chat_url = "https://wewordle.org/gptapi/v1/web/turbo"
|
|
57
|
+
self.limit_url = "https://wewordle.org/gptapi/v1/web/get_limit"
|
|
58
|
+
|
|
59
|
+
# Initialize LitAgent for user agent generation
|
|
60
|
+
self.agent = LitAgent()
|
|
61
|
+
|
|
62
|
+
# Generate browser fingerprint
|
|
63
|
+
self.fingerprint = self.agent.generate_fingerprint(browser="chrome")
|
|
64
|
+
|
|
65
|
+
# Setup headers to mimic browser requests
|
|
66
|
+
self.headers = {
|
|
67
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
|
|
68
|
+
'Accept': 'application/json',
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'sec-ch-ua': '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
|
|
71
|
+
'sec-ch-ua-mobile': '?0',
|
|
72
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
73
|
+
'Referer': 'https://claude.online/',
|
|
74
|
+
'Origin': 'https://claude.online',
|
|
75
|
+
'Sec-Fetch-Dest': 'empty',
|
|
76
|
+
'Sec-Fetch-Mode': 'cors',
|
|
77
|
+
'Sec-Fetch-Site': 'cross-site',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Initialize curl_cffi Session
|
|
81
|
+
self.session = Session()
|
|
82
|
+
self.session.headers.update(self.headers)
|
|
83
|
+
self.session.proxies = proxies
|
|
84
|
+
|
|
85
|
+
self.system_prompt = system_prompt
|
|
86
|
+
self.is_conversation = is_conversation
|
|
87
|
+
self.max_tokens_to_sample = max_tokens
|
|
88
|
+
self.timeout = timeout
|
|
89
|
+
self.last_response = {}
|
|
90
|
+
self.model = model
|
|
91
|
+
|
|
92
|
+
self.__available_optimizers = (
|
|
93
|
+
method
|
|
94
|
+
for method in dir(Optimizers)
|
|
95
|
+
if callable(getattr(Optimizers, method)) and not method.startswith("__")
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
Conversation.intro = (
|
|
99
|
+
AwesomePrompts().get_act(
|
|
100
|
+
act, raise_not_found=True, default=None, case_insensitive=True
|
|
101
|
+
)
|
|
102
|
+
if act
|
|
103
|
+
else intro or Conversation.intro
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.conversation = Conversation(
|
|
107
|
+
is_conversation, self.max_tokens_to_sample, filepath, update_file
|
|
108
|
+
)
|
|
109
|
+
self.conversation.history_offset = history_offset
|
|
110
|
+
|
|
111
|
+
def _make_request(self, url: str, payload: Optional[Dict] = None, method: str = "POST") -> Dict:
|
|
112
|
+
"""
|
|
113
|
+
Make a request to the API with error handling.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
url: The URL to request
|
|
117
|
+
payload: Request payload (for POST requests)
|
|
118
|
+
method: HTTP method
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Parsed JSON response
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
FailedToGenerateResponseError: If request fails
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
if method == "POST":
|
|
128
|
+
response = self.session.post(
|
|
129
|
+
url,
|
|
130
|
+
json=payload,
|
|
131
|
+
timeout=self.timeout,
|
|
132
|
+
impersonate="chrome110"
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
response = self.session.get(
|
|
136
|
+
url,
|
|
137
|
+
timeout=self.timeout,
|
|
138
|
+
impersonate="chrome110"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
response.raise_for_status()
|
|
142
|
+
|
|
143
|
+
# Check rate limit headers
|
|
144
|
+
if 'ratelimit-remaining' in response.headers:
|
|
145
|
+
remaining = int(response.headers['ratelimit-remaining'])
|
|
146
|
+
if remaining <= 0:
|
|
147
|
+
reset_time = int(response.headers.get('ratelimit-reset', 60))
|
|
148
|
+
raise exceptions.FailedToGenerateResponseError(
|
|
149
|
+
f"Rate limit exceeded. Resets in {reset_time} seconds."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return response.json()
|
|
153
|
+
|
|
154
|
+
except CurlError as e:
|
|
155
|
+
raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {str(e)}")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise exceptions.FailedToGenerateResponseError(f"Request failed ({type(e).__name__}): {str(e)}")
|
|
158
|
+
|
|
159
|
+
def get_remaining_limit(self) -> Dict[str, Union[int, float]]:
|
|
160
|
+
"""
|
|
161
|
+
Get the current rate limit status.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dict containing limit information:
|
|
165
|
+
- limit: Remaining requests
|
|
166
|
+
- fullLimit: Unknown parameter (possibly daily limit)
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
response = self._make_request(self.limit_url, method="GET")
|
|
170
|
+
return {
|
|
171
|
+
"limit": response.get("limit", 0),
|
|
172
|
+
"fullLimit": response.get("fullLimit", 0)
|
|
173
|
+
}
|
|
174
|
+
except Exception:
|
|
175
|
+
# Return default values if request fails
|
|
176
|
+
return {"limit": 0, "fullLimit": 0}
|
|
177
|
+
|
|
178
|
+
def create_message(self, content: str, role: str = "user") -> Dict[str, str]:
|
|
179
|
+
"""
|
|
180
|
+
Create a message dictionary.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
content: Message content
|
|
184
|
+
role: Message role (user/assistant)
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Message dictionary
|
|
188
|
+
"""
|
|
189
|
+
return {
|
|
190
|
+
"content": content,
|
|
191
|
+
"role": role
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def ask(
|
|
195
|
+
self,
|
|
196
|
+
prompt: str,
|
|
197
|
+
stream: bool = False,
|
|
198
|
+
raw: bool = False,
|
|
199
|
+
optimizer: str = None,
|
|
200
|
+
conversationally: bool = False,
|
|
201
|
+
) -> Union[Dict[str, Any], Generator]:
|
|
202
|
+
"""
|
|
203
|
+
Send a chat message and get response.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
prompt: The message to send
|
|
207
|
+
stream: Whether to stream the response (not supported by this API)
|
|
208
|
+
raw: Whether to return raw response
|
|
209
|
+
optimizer: Optimizer to use for the prompt
|
|
210
|
+
conversationally: Whether to generate the prompt conversationally
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dict containing response or Generator for streaming
|
|
214
|
+
"""
|
|
215
|
+
conversation_prompt = self.conversation.gen_complete_prompt(prompt)
|
|
216
|
+
if optimizer:
|
|
217
|
+
if optimizer in self.__available_optimizers:
|
|
218
|
+
conversation_prompt = getattr(Optimizers, optimizer)(
|
|
219
|
+
conversation_prompt if conversationally else prompt
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
raise Exception(f"Optimizer is not one of {self.__available_optimizers}")
|
|
223
|
+
|
|
224
|
+
# Prepare messages (only current message supported by this API)
|
|
225
|
+
messages = [self.create_message(conversation_prompt, "user")]
|
|
226
|
+
|
|
227
|
+
payload = {
|
|
228
|
+
"messages": messages
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def for_stream():
|
|
232
|
+
# This API doesn't support streaming, so simulate it
|
|
233
|
+
try:
|
|
234
|
+
response = self._make_request(self.chat_url, payload)
|
|
235
|
+
|
|
236
|
+
# Extract message content
|
|
237
|
+
if "message" in response and "content" in response["message"]:
|
|
238
|
+
content = response["message"]["content"]
|
|
239
|
+
|
|
240
|
+
# Simulate streaming by yielding chunks
|
|
241
|
+
words = content.split()
|
|
242
|
+
current_chunk = ""
|
|
243
|
+
for word in words:
|
|
244
|
+
current_chunk += word + " "
|
|
245
|
+
if len(current_chunk) > 50: # Yield chunks of ~50 characters
|
|
246
|
+
yield dict(text=current_chunk.strip())
|
|
247
|
+
current_chunk = ""
|
|
248
|
+
if current_chunk:
|
|
249
|
+
yield dict(text=current_chunk.strip())
|
|
250
|
+
|
|
251
|
+
self.last_response = {"text": content}
|
|
252
|
+
self.conversation.update_chat_history(prompt, content)
|
|
253
|
+
else:
|
|
254
|
+
raise exceptions.FailedToGenerateResponseError("Unexpected response format")
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
raise exceptions.FailedToGenerateResponseError(f"Chat request failed: {str(e)}")
|
|
258
|
+
|
|
259
|
+
def for_non_stream():
|
|
260
|
+
try:
|
|
261
|
+
response = self._make_request(self.chat_url, payload)
|
|
262
|
+
|
|
263
|
+
# Extract message content
|
|
264
|
+
if "message" in response and "content" in response["message"]:
|
|
265
|
+
content = response["message"]["content"]
|
|
266
|
+
self.last_response = {"text": content}
|
|
267
|
+
self.conversation.update_chat_history(prompt, content)
|
|
268
|
+
return self.last_response if not raw else content
|
|
269
|
+
else:
|
|
270
|
+
raise exceptions.FailedToGenerateResponseError("Unexpected response format")
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
raise exceptions.FailedToGenerateResponseError(f"Chat request failed: {str(e)}")
|
|
274
|
+
|
|
275
|
+
return for_stream() if stream else for_non_stream()
|
|
276
|
+
|
|
277
|
+
def chat(
|
|
278
|
+
self,
|
|
279
|
+
prompt: str,
|
|
280
|
+
stream: bool = False,
|
|
281
|
+
optimizer: str = None,
|
|
282
|
+
conversationally: bool = False,
|
|
283
|
+
) -> Union[str, Generator[str, None, None]]:
|
|
284
|
+
"""
|
|
285
|
+
Generate a response from Claude Online.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
prompt: The prompt to send
|
|
289
|
+
stream: Whether to stream the response
|
|
290
|
+
optimizer: Optimizer to use
|
|
291
|
+
conversationally: Whether to generate conversationally
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Response string or generator for streaming
|
|
295
|
+
"""
|
|
296
|
+
def for_stream_chat():
|
|
297
|
+
for response in self.ask(
|
|
298
|
+
prompt, stream=True, raw=False,
|
|
299
|
+
optimizer=optimizer, conversationally=conversationally
|
|
300
|
+
):
|
|
301
|
+
yield self.get_message(response)
|
|
302
|
+
|
|
303
|
+
def for_non_stream_chat():
|
|
304
|
+
response_data = self.ask(
|
|
305
|
+
prompt, stream=False, raw=False,
|
|
306
|
+
optimizer=optimizer, conversationally=conversationally
|
|
307
|
+
)
|
|
308
|
+
return self.get_message(response_data)
|
|
309
|
+
|
|
310
|
+
return for_stream_chat() if stream else for_non_stream_chat()
|
|
311
|
+
|
|
312
|
+
def get_message(self, response: dict) -> str:
|
|
313
|
+
"""
|
|
314
|
+
Extract message from response.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
response: Response dictionary
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Message content
|
|
321
|
+
"""
|
|
322
|
+
assert isinstance(response, dict), "Response should be of dict data-type only"
|
|
323
|
+
return response["text"]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
print("-" * 80)
|
|
328
|
+
print(f"{'Model':<50} {'Status':<10} {'Response'}")
|
|
329
|
+
print("-" * 80)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
ai = ClaudeOnline(timeout=60)
|
|
333
|
+
limits = ai.get_remaining_limit()
|
|
334
|
+
print(f"Rate limits - Remaining: {limits['limit']}, Full limit: {limits['fullLimit']}")
|
|
335
|
+
|
|
336
|
+
if limits['limit'] > 0:
|
|
337
|
+
response = ai.chat("Say 'Hello World' in one word", stream=False)
|
|
338
|
+
if response and len(response.strip()) > 0:
|
|
339
|
+
status = "✓"
|
|
340
|
+
clean_text = response.strip().encode('utf-8', errors='ignore').decode('utf-8')
|
|
341
|
+
display_text = clean_text[:50] + "..." if len(clean_text) > 50 else clean_text
|
|
342
|
+
else:
|
|
343
|
+
status = "✗"
|
|
344
|
+
display_text = "Empty response"
|
|
345
|
+
print(f"{'claude-online':<50} {status:<10} {display_text}")
|
|
346
|
+
else:
|
|
347
|
+
print(f"{'claude-online':<50} {'✗':<10} Rate limit exceeded")
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
print(f"{'claude-online':<50} {'✗':<10} {str(e)}")
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from requests.exceptions import RequestException
|
|
11
|
+
|
|
12
|
+
from webscout.litagent import LitAgent
|
|
13
|
+
from webscout.Provider.TTI.base import BaseImages, TTICompatibleProvider
|
|
14
|
+
from webscout.Provider.TTI.utils import ImageData, ImageResponse
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from PIL import Image
|
|
18
|
+
except ImportError:
|
|
19
|
+
Image = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Images(BaseImages):
|
|
23
|
+
def __init__(self, client):
|
|
24
|
+
self._client = client
|
|
25
|
+
|
|
26
|
+
def create(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
model: str,
|
|
30
|
+
prompt: str,
|
|
31
|
+
n: int = 1,
|
|
32
|
+
size: str = "1024x1024",
|
|
33
|
+
response_format: str = "url",
|
|
34
|
+
user: Optional[str] = None,
|
|
35
|
+
style: str = "none",
|
|
36
|
+
aspect_ratio: str = "1:1",
|
|
37
|
+
timeout: int = 60,
|
|
38
|
+
image_format: str = "png",
|
|
39
|
+
seed: Optional[int] = None,
|
|
40
|
+
**kwargs,
|
|
41
|
+
) -> ImageResponse:
|
|
42
|
+
"""
|
|
43
|
+
Generate images using Claude Online's /imagine feature via Pollinations.ai.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
model: Model to use (ignored, uses Pollinations.ai)
|
|
47
|
+
prompt: The image generation prompt
|
|
48
|
+
n: Number of images to generate (max 1 for Claude Online)
|
|
49
|
+
size: Image size (supports various sizes)
|
|
50
|
+
response_format: "url" or "b64_json"
|
|
51
|
+
timeout: Request timeout in seconds
|
|
52
|
+
image_format: Output format "png" or "jpeg"
|
|
53
|
+
**kwargs: Additional parameters
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
ImageResponse with generated image data
|
|
57
|
+
"""
|
|
58
|
+
if Image is None:
|
|
59
|
+
raise ImportError("Pillow (PIL) is required for image format conversion.")
|
|
60
|
+
|
|
61
|
+
# Claude Online only supports 1 image per request
|
|
62
|
+
if n > 1:
|
|
63
|
+
raise ValueError("Claude Online only supports generating 1 image per request")
|
|
64
|
+
|
|
65
|
+
# Parse size parameter
|
|
66
|
+
width, height = self._parse_size(size)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Clean the prompt (remove command words if present)
|
|
70
|
+
clean_prompt = self._clean_prompt(prompt)
|
|
71
|
+
|
|
72
|
+
# Generate image using Pollinations.ai API
|
|
73
|
+
timestamp = int(time.time() * 1000) # Use timestamp as seed for uniqueness
|
|
74
|
+
seed_value = seed if seed is not None else timestamp
|
|
75
|
+
|
|
76
|
+
# Build the Pollinations.ai URL
|
|
77
|
+
base_url = "https://image.pollinations.ai/prompt"
|
|
78
|
+
params = {
|
|
79
|
+
"width": width,
|
|
80
|
+
"height": height,
|
|
81
|
+
"nologo": "true",
|
|
82
|
+
"seed": seed_value
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
image_url = f"{base_url}/{clean_prompt}"
|
|
86
|
+
query_params = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
87
|
+
full_image_url = f"{image_url}?{query_params}"
|
|
88
|
+
|
|
89
|
+
# Download the image
|
|
90
|
+
response = requests.get(full_image_url, timeout=timeout, stream=True)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
|
|
93
|
+
img_bytes = response.content
|
|
94
|
+
|
|
95
|
+
# Convert image format if needed
|
|
96
|
+
with BytesIO(img_bytes) as input_io:
|
|
97
|
+
with Image.open(input_io) as im:
|
|
98
|
+
out_io = BytesIO()
|
|
99
|
+
if image_format.lower() == "jpeg":
|
|
100
|
+
im = im.convert("RGB")
|
|
101
|
+
im.save(out_io, format="JPEG")
|
|
102
|
+
else:
|
|
103
|
+
im.save(out_io, format="PNG")
|
|
104
|
+
processed_img_bytes = out_io.getvalue()
|
|
105
|
+
|
|
106
|
+
# Handle response format
|
|
107
|
+
if response_format == "url":
|
|
108
|
+
# Upload to image hosting service
|
|
109
|
+
uploaded_url = self._upload_image(processed_img_bytes, image_format)
|
|
110
|
+
if not uploaded_url:
|
|
111
|
+
raise RuntimeError("Failed to upload generated image")
|
|
112
|
+
result_data = [ImageData(url=uploaded_url)]
|
|
113
|
+
elif response_format == "b64_json":
|
|
114
|
+
b64 = base64.b64encode(processed_img_bytes).decode("utf-8")
|
|
115
|
+
result_data = [ImageData(b64_json=b64)]
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError("response_format must be 'url' or 'b64_json'")
|
|
118
|
+
|
|
119
|
+
return ImageResponse(created=int(time.time()), data=result_data)
|
|
120
|
+
|
|
121
|
+
except RequestException as e:
|
|
122
|
+
raise RuntimeError(f"Failed to generate image with Claude Online: {e}")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise RuntimeError(f"Unexpected error during image generation: {e}")
|
|
125
|
+
|
|
126
|
+
def _parse_size(self, size: str) -> tuple[int, int]:
|
|
127
|
+
"""Parse size string into width and height."""
|
|
128
|
+
size = size.lower().strip()
|
|
129
|
+
|
|
130
|
+
# Handle common size formats
|
|
131
|
+
size_map = {
|
|
132
|
+
"256x256": (256, 256),
|
|
133
|
+
"512x512": (512, 512),
|
|
134
|
+
"1024x1024": (1024, 1024),
|
|
135
|
+
"1024x768": (1024, 768),
|
|
136
|
+
"768x1024": (768, 1024),
|
|
137
|
+
"1280x720": (1280, 720),
|
|
138
|
+
"720x1280": (720, 1280),
|
|
139
|
+
"1920x1080": (1920, 1080),
|
|
140
|
+
"1080x1920": (1080, 1920),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if size in size_map:
|
|
144
|
+
return size_map[size]
|
|
145
|
+
|
|
146
|
+
# Try to parse custom size (e.g., "800x600")
|
|
147
|
+
try:
|
|
148
|
+
width, height = size.split("x")
|
|
149
|
+
return int(width), int(height)
|
|
150
|
+
except (ValueError, AttributeError):
|
|
151
|
+
# Default to 1024x1024
|
|
152
|
+
return 1024, 1024
|
|
153
|
+
|
|
154
|
+
def _clean_prompt(self, prompt: str) -> str:
|
|
155
|
+
"""Clean the prompt by removing command prefixes."""
|
|
156
|
+
# Remove common image generation command prefixes
|
|
157
|
+
prefixes_to_remove = [
|
|
158
|
+
r'^/imagine\s*',
|
|
159
|
+
r'^/image\s*',
|
|
160
|
+
r'^/picture\s*',
|
|
161
|
+
r'^/draw\s*',
|
|
162
|
+
r'^/create\s*',
|
|
163
|
+
r'^/generate\s*',
|
|
164
|
+
r'^создай изображение\s*',
|
|
165
|
+
r'^нарисуй\s*',
|
|
166
|
+
r'^сгенерируй картинку\s*',
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
import re
|
|
170
|
+
clean_prompt = prompt
|
|
171
|
+
for prefix in prefixes_to_remove:
|
|
172
|
+
clean_prompt = re.sub(prefix, '', clean_prompt, flags=re.IGNORECASE)
|
|
173
|
+
|
|
174
|
+
return clean_prompt.strip()
|
|
175
|
+
|
|
176
|
+
def _upload_image(self, img_bytes: bytes, image_format: str, max_retries: int = 3) -> Optional[str]:
|
|
177
|
+
"""Upload image to hosting service and return URL"""
|
|
178
|
+
|
|
179
|
+
def upload_to_catbox(img_bytes, image_format):
|
|
180
|
+
"""Upload to catbox.moe"""
|
|
181
|
+
ext = "jpg" if image_format.lower() == "jpeg" else "png"
|
|
182
|
+
tmp_path = None
|
|
183
|
+
try:
|
|
184
|
+
with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as tmp:
|
|
185
|
+
tmp.write(img_bytes)
|
|
186
|
+
tmp.flush()
|
|
187
|
+
tmp_path = tmp.name
|
|
188
|
+
|
|
189
|
+
with open(tmp_path, "rb") as f:
|
|
190
|
+
files = {"fileToUpload": (f"image.{ext}", f, f"image/{ext}")}
|
|
191
|
+
data = {"reqtype": "fileupload", "json": "true"}
|
|
192
|
+
headers = {"User-Agent": LitAgent().random()}
|
|
193
|
+
|
|
194
|
+
resp = requests.post(
|
|
195
|
+
"https://catbox.moe/user/api.php",
|
|
196
|
+
files=files,
|
|
197
|
+
data=data,
|
|
198
|
+
headers=headers,
|
|
199
|
+
timeout=30,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if resp.status_code == 200 and resp.text.strip():
|
|
203
|
+
text = resp.text.strip()
|
|
204
|
+
if text.startswith("http"):
|
|
205
|
+
return text
|
|
206
|
+
try:
|
|
207
|
+
result = resp.json()
|
|
208
|
+
if "url" in result:
|
|
209
|
+
return result["url"]
|
|
210
|
+
except json.JSONDecodeError:
|
|
211
|
+
pass
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
finally:
|
|
215
|
+
if tmp_path and os.path.isfile(tmp_path):
|
|
216
|
+
try:
|
|
217
|
+
os.remove(tmp_path)
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def upload_to_0x0(img_bytes, image_format):
|
|
223
|
+
"""Upload to 0x0.st as fallback"""
|
|
224
|
+
ext = "jpg" if image_format.lower() == "jpeg" else "png"
|
|
225
|
+
tmp_path = None
|
|
226
|
+
try:
|
|
227
|
+
with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as tmp:
|
|
228
|
+
tmp.write(img_bytes)
|
|
229
|
+
tmp.flush()
|
|
230
|
+
tmp_path = tmp.name
|
|
231
|
+
|
|
232
|
+
with open(tmp_path, "rb") as img_file:
|
|
233
|
+
files = {"file": img_file}
|
|
234
|
+
response = requests.post("https://0x0.st", files=files, timeout=30)
|
|
235
|
+
response.raise_for_status()
|
|
236
|
+
image_url = response.text.strip()
|
|
237
|
+
if image_url.startswith("http"):
|
|
238
|
+
return image_url
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
finally:
|
|
242
|
+
if tmp_path and os.path.isfile(tmp_path):
|
|
243
|
+
try:
|
|
244
|
+
os.remove(tmp_path)
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
# Try primary upload method
|
|
250
|
+
for attempt in range(max_retries):
|
|
251
|
+
uploaded_url = upload_to_catbox(img_bytes, image_format)
|
|
252
|
+
if uploaded_url:
|
|
253
|
+
return uploaded_url
|
|
254
|
+
time.sleep(1 * (attempt + 1))
|
|
255
|
+
|
|
256
|
+
# Try fallback method
|
|
257
|
+
for attempt in range(max_retries):
|
|
258
|
+
uploaded_url = upload_to_0x0(img_bytes, image_format)
|
|
259
|
+
if uploaded_url:
|
|
260
|
+
return uploaded_url
|
|
261
|
+
time.sleep(1 * (attempt + 1))
|
|
262
|
+
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class ClaudeOnlineTTI(TTICompatibleProvider):
|
|
267
|
+
"""
|
|
268
|
+
Claude Online Text-to-Image Provider
|
|
269
|
+
|
|
270
|
+
Uses Claude Online's /imagine feature with Pollinations.ai backend.
|
|
271
|
+
Supports high-quality image generation with various styles and sizes.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
AVAILABLE_MODELS = ["claude-imagine"]
|
|
275
|
+
|
|
276
|
+
def __init__(self):
|
|
277
|
+
self.api_endpoint = "https://image.pollinations.ai/prompt"
|
|
278
|
+
self.session = requests.Session()
|
|
279
|
+
self.user_agent = LitAgent().random()
|
|
280
|
+
self.headers = {
|
|
281
|
+
"accept": "image/*",
|
|
282
|
+
"accept-language": "en-US,en;q=0.9",
|
|
283
|
+
"user-agent": self.user_agent,
|
|
284
|
+
}
|
|
285
|
+
self.session.headers.update(self.headers)
|
|
286
|
+
self.images = Images(self)
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def models(self):
|
|
290
|
+
class _ModelList:
|
|
291
|
+
def list(inner_self):
|
|
292
|
+
return type(self).AVAILABLE_MODELS
|
|
293
|
+
|
|
294
|
+
return _ModelList()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
from rich import print
|
|
299
|
+
|
|
300
|
+
# Test the Claude Online TTI provider
|
|
301
|
+
client = ClaudeOnlineTTI()
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
response = client.images.create(
|
|
305
|
+
model="claude-imagine",
|
|
306
|
+
prompt="a beautiful sunset over mountains with vibrant colors",
|
|
307
|
+
response_format="url",
|
|
308
|
+
timeout=60,
|
|
309
|
+
)
|
|
310
|
+
print("✅ Image generation successful!")
|
|
311
|
+
print(response)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
print(f"❌ Image generation failed: {e}")
|
|
314
|
+
import traceback
|
|
315
|
+
traceback.print_exc()
|
webscout/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "2025.10.
|
|
1
|
+
__version__ = "2025.10.17"
|
|
2
2
|
__prog__ = "webscout"
|
webscout/version.py.bak
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "2025.10.
|
|
1
|
+
__version__ = "2025.10.16"
|
|
2
2
|
__prog__ = "webscout"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webscout
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.17
|
|
4
4
|
Summary: Search for anything using Google, DuckDuckGo, phind.com, Contains AI models, can transcribe yt videos, temporary email and phone number generation, has TTS support, webai (terminal gpt and open interpreter) and offline LLMs and more
|
|
5
5
|
Author-email: OEvortex <helpingai5@gmail.com>
|
|
6
6
|
License: HelpingAI
|
|
@@ -50,7 +50,6 @@ Requires-Dist: lxml>=5.2.2
|
|
|
50
50
|
Requires-Dist: orjson
|
|
51
51
|
Requires-Dist: PyYAML
|
|
52
52
|
Requires-Dist: pillow
|
|
53
|
-
Requires-Dist: bson
|
|
54
53
|
Requires-Dist: cloudscraper
|
|
55
54
|
Requires-Dist: html5lib
|
|
56
55
|
Requires-Dist: aiofiles
|
|
@@ -163,7 +162,7 @@ Dynamic: license-file
|
|
|
163
162
|
- **[YouTube Toolkit](webscout/Extra/YTToolkit/README.md):** Advanced YouTube video and transcript management with multi-language support
|
|
164
163
|
- **[Text-to-Speech (TTS)](webscout/Provider/TTS/README.md):** Convert text into natural-sounding speech using multiple AI-powered providers
|
|
165
164
|
- **[Text-to-Image](webscout/Provider/TTI/README.md):** Generate high-quality images using a wide range of AI art providers
|
|
166
|
-
- **[Weather Tools](
|
|
165
|
+
- **[Weather Tools](docs/weather.md):** Retrieve detailed weather information for any location
|
|
167
166
|
</p>
|
|
168
167
|
</details>
|
|
169
168
|
|
|
@@ -14,13 +14,12 @@ webscout/prompt_manager.py,sha256=ysKFgPhkV3uqrOCilqcS9rG8xhzdU_d2wx0grC9WCCc,98
|
|
|
14
14
|
webscout/sanitize.py,sha256=pw2Dzn-Jw9mOD4mpALYAvAf-medA-9AqdzsOmdXQbl0,46577
|
|
15
15
|
webscout/update_checker.py,sha256=bz0TzRxip9DOIVMFyNz9HsGj4RKB0xZgo57AUVSJINo,3708
|
|
16
16
|
webscout/utils.py,sha256=o2hU3qaVPk25sog3e4cyVZO3l8xwaZpYRziZPotEzNo,3075
|
|
17
|
-
webscout/version.py,sha256=
|
|
18
|
-
webscout/version.py.bak,sha256=
|
|
17
|
+
webscout/version.py,sha256=3D9mFWY4P-ZGCKSuDPkkzHusmyndScdoNbUtry1vdCE,51
|
|
18
|
+
webscout/version.py.bak,sha256=bau6psbS6y1YovrRr6K7N3uBj_p0yf9WmHDqSK16bMs,51
|
|
19
19
|
webscout/Extra/Act.md,sha256=_C2VW_Dc-dc7eejpGYKAOZhImHKPiQ7NSwE3bkzr6fg,18952
|
|
20
20
|
webscout/Extra/__init__.py,sha256=KvJRsRBRO-fZp2jSCl6KQnPppi93hriA6O_U1O1s31c,177
|
|
21
21
|
webscout/Extra/gguf.md,sha256=McXGz5sTfzOO9X4mH8yIqu5K3CgjzyXKi4_HQtezdZ4,12435
|
|
22
22
|
webscout/Extra/gguf.py,sha256=sbUsEPbws99yzS2EzZHLmZ55q6esFfu7k2y5WzMBEbA,55683
|
|
23
|
-
webscout/Extra/weather.md,sha256=CaDwLmmHEXHSsMkSxFzrNOl2YslRcIMn3Hd3N1eaFq4,9781
|
|
24
23
|
webscout/Extra/weather.py,sha256=KwbJC2Most6jVLvv8X_Ct7APGimSr98REgLc07kR-Ec,7648
|
|
25
24
|
webscout/Extra/weather_ascii.py,sha256=k6U0F9w5xXPvYK-Wq3pjUh7W0gGJG4e_YhxeUI65Vpk,2612
|
|
26
25
|
webscout/Extra/GitToolkit/__init__.py,sha256=S8clC97xkihcsxfbAO9QOmSO2ljLOiQFoUh_klhp2zw,146
|
|
@@ -68,6 +67,7 @@ webscout/Provider/Andi.py,sha256=CWFMRw9edQ0yRGLXtciPsl_O0v2Qs80TH8TkJ2bkAUk,873
|
|
|
68
67
|
webscout/Provider/Apriel.py,sha256=caAjFQtMLfB0Er_eA-tW8_kGASE5AnaqLYifw1uJuIo,11335
|
|
69
68
|
webscout/Provider/ChatGPTClone.py,sha256=hX_9Ytegiz6oDARzjwU11PPEXJqwTFGHAgShtDLbGHs,9715
|
|
70
69
|
webscout/Provider/ChatSandbox.py,sha256=Hl8vOQzij7VyYVoL3DvJO6HGUs6tXZY3xrbCLKrF_ZI,13122
|
|
70
|
+
webscout/Provider/ClaudeOnline.py,sha256=3J5LEjvxzpYgIcycCq1aG_kFjks7ECkJS6l0HQ5bEyQ,12748
|
|
71
71
|
webscout/Provider/Cloudflare.py,sha256=nrHCZ9SYNNRIxxzR_QRU1fy-jh31WnErxIimF0aDZms,14155
|
|
72
72
|
webscout/Provider/Cohere.py,sha256=wPULeG_2JZdhN8oTBjs_QNqs6atjkYkjCa01mRmg8Fw,8082
|
|
73
73
|
webscout/Provider/Deepinfra.py,sha256=Z3FNMaaVd4KiitDG8LBgGWycNuT6Y1Z06sCFURd0Ynw,15882
|
|
@@ -193,6 +193,7 @@ webscout/Provider/TTI/__init__.py,sha256=EwtLtQs6VEpLFOBeXi9hC9FTJIJdHPZTZtu3iFF
|
|
|
193
193
|
webscout/Provider/TTI/aiarta.py,sha256=aWIATT_xQU7AmH3rK9mDCrUyN_7yOqkpXFPKicrGKfY,14956
|
|
194
194
|
webscout/Provider/TTI/base.py,sha256=SnNs-mJH8tcmcygNnfNtwPvWBk11FUO6nLTfnHnZADQ,5088
|
|
195
195
|
webscout/Provider/TTI/bing.py,sha256=wzUtAprlptlvuA3wndn3i-5UBQIL-YSe8rmjhqb1T-Q,10632
|
|
196
|
+
webscout/Provider/TTI/claudeonline.py,sha256=v6CEst37QG53g-H9rqjwiZeipJyedU1HHG3PrHX31Bw,10969
|
|
196
197
|
webscout/Provider/TTI/gpt1image.py,sha256=PKqc0JU7hweypnSooI7gWuXqJbKz52xfGQny90-Uin0,4831
|
|
197
198
|
webscout/Provider/TTI/imagen.py,sha256=VtSTJzTao4oUje4viEuYbcAw2Fol-JhBPax3XeXuByM,6488
|
|
198
199
|
webscout/Provider/TTI/infip.py,sha256=qAzcym5hyxHyuW5PFqcWN__ebs5s9wLCADakO_28aD4,7083
|
|
@@ -328,9 +329,9 @@ webscout/zeroart/__init__.py,sha256=Cy9AUtXnOaFBQjNvCpN19IXJo7Lg15VTaNcTBxOTFek,
|
|
|
328
329
|
webscout/zeroart/base.py,sha256=I-xhDEfArBb6q7hiF5oPoyXeu2hzL6orp7uWgS_YtG8,2299
|
|
329
330
|
webscout/zeroart/effects.py,sha256=XUNZY1-wMPd6GNL3glFXtWaF9wDis_z55qTyCdnzHDo,5063
|
|
330
331
|
webscout/zeroart/fonts.py,sha256=S7qDhUmDXl1makMreZl_eVW_7-sqVQiGn-kQKl0Hg_A,51006
|
|
331
|
-
webscout-2025.10.
|
|
332
|
-
webscout-2025.10.
|
|
333
|
-
webscout-2025.10.
|
|
334
|
-
webscout-2025.10.
|
|
335
|
-
webscout-2025.10.
|
|
336
|
-
webscout-2025.10.
|
|
332
|
+
webscout-2025.10.17.dist-info/licenses/LICENSE.md,sha256=hyfFlVn7pWcrvuvs-piB8k4J8DlXdOsYje9RyPxc6Ik,7543
|
|
333
|
+
webscout-2025.10.17.dist-info/METADATA,sha256=-12NEKcAbsCMUnFV4pl40tKrXhXDW0skcWCfCA1vhG8,21660
|
|
334
|
+
webscout-2025.10.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
335
|
+
webscout-2025.10.17.dist-info/entry_points.txt,sha256=4xAgKHWwNhAvJyShLCFs_IU8Reb8zR3wqf8egrsDr8g,118
|
|
336
|
+
webscout-2025.10.17.dist-info/top_level.txt,sha256=nYIw7OKBQDr_Z33IzZUKidRD3zQEo8jOJYkMVMeN334,9
|
|
337
|
+
webscout-2025.10.17.dist-info/RECORD,,
|
webscout/Extra/weather.md
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<h1>☀️ Webscout Weather Toolkit</h1>
|
|
3
|
-
<p><strong>Comprehensive weather data retrieval and visualization tools</strong></p>
|
|
4
|
-
|
|
5
|
-
<!-- Badges -->
|
|
6
|
-
<p>
|
|
7
|
-
<a href="#-installation"><img src="https://img.shields.io/badge/Easy-Installation-success?style=flat-square" alt="Easy Installation"></a>
|
|
8
|
-
<a href="#-current-weather-data"><img src="https://img.shields.io/badge/Real--time-Data-blue?style=flat-square" alt="Real-time Data"></a>
|
|
9
|
-
<a href="#-ascii-art-weather"><img src="https://img.shields.io/badge/ASCII-Visualization-orange?style=flat-square" alt="ASCII Visualization"></a>
|
|
10
|
-
</p>
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
|
-
> [!NOTE]
|
|
14
|
-
> Webscout's Weather Toolkit provides powerful tools to retrieve and display weather information in various formats, including structured data and ASCII art visualization.
|
|
15
|
-
|
|
16
|
-
## 📋 Table of Contents
|
|
17
|
-
|
|
18
|
-
- [🚀 Installation](#-installation)
|
|
19
|
-
- [🌡️ Current Weather Data](#-current-weather-data)
|
|
20
|
-
- [🔮 Weather Forecast](#-weather-forecast)
|
|
21
|
-
- [🎨 ASCII Art Weather](#-ascii-art-weather)
|
|
22
|
-
- [📊 Data Structure](#-data-structure)
|
|
23
|
-
- [⚙️ Advanced Usage](#-advanced-usage)
|
|
24
|
-
|
|
25
|
-
## 🚀 Installation
|
|
26
|
-
|
|
27
|
-
The Weather Toolkit is included in the Webscout package. Install or update Webscout to get access:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
pip install -U webscout
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## 🌡️ Current Weather Data
|
|
34
|
-
|
|
35
|
-
Retrieve structured current weather information for any location worldwide.
|
|
36
|
-
|
|
37
|
-
```python
|
|
38
|
-
from webscout.Extra import weather
|
|
39
|
-
|
|
40
|
-
# Get weather for a specific location
|
|
41
|
-
forecast = weather.get("London")
|
|
42
|
-
|
|
43
|
-
# Access current conditions
|
|
44
|
-
print(f"Current weather: {forecast.summary}")
|
|
45
|
-
|
|
46
|
-
# Access temperature details
|
|
47
|
-
if forecast.current_condition:
|
|
48
|
-
print(f"Temperature: {forecast.current_condition.temp_c}°C / {forecast.current_condition.temp_f}°F")
|
|
49
|
-
print(f"Feels like: {forecast.current_condition.feels_like_c}°C")
|
|
50
|
-
print(f"Conditions: {forecast.current_condition.weather_desc}")
|
|
51
|
-
print(f"Wind: {forecast.current_condition.wind_speed_kmph} km/h from {forecast.current_condition.wind_direction}")
|
|
52
|
-
print(f"Humidity: {forecast.current_condition.humidity}%")
|
|
53
|
-
print(f"Visibility: {forecast.current_condition.visibility} km")
|
|
54
|
-
print(f"Pressure: {forecast.current_condition.pressure} mb")
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## 🔮 Weather Forecast
|
|
58
|
-
|
|
59
|
-
Access detailed forecast information for today and upcoming days.
|
|
60
|
-
|
|
61
|
-
```python
|
|
62
|
-
from webscout.Extra import weather
|
|
63
|
-
|
|
64
|
-
forecast = weather.get("Tokyo")
|
|
65
|
-
|
|
66
|
-
# Today's forecast
|
|
67
|
-
if forecast.today:
|
|
68
|
-
print(f"Today ({forecast.today.date_formatted}):")
|
|
69
|
-
print(f" Temperature range: {forecast.today.min_temp_c}°C - {forecast.today.max_temp_c}°C")
|
|
70
|
-
print(f" Sunrise: {forecast.today.sunrise}, Sunset: {forecast.today.sunset}")
|
|
71
|
-
|
|
72
|
-
# Access hourly forecasts
|
|
73
|
-
if forecast.today.hourly and len(forecast.today.hourly) > 4:
|
|
74
|
-
noon = forecast.today.hourly[4] # Noon (12:00) is usually index 4
|
|
75
|
-
print(f" Noon conditions: {noon.weather_desc}")
|
|
76
|
-
print(f" Chance of rain: {noon.chance_of_rain}%")
|
|
77
|
-
print(f" Humidity: {noon.humidity}%")
|
|
78
|
-
print(f" UV Index: {noon.uv_index}")
|
|
79
|
-
|
|
80
|
-
# Tomorrow's forecast
|
|
81
|
-
if forecast.tomorrow:
|
|
82
|
-
print(f"\nTomorrow ({forecast.tomorrow.date_formatted}):")
|
|
83
|
-
print(f" Temperature range: {forecast.tomorrow.min_temp_c}°C - {forecast.tomorrow.max_temp_c}°C")
|
|
84
|
-
print(f" Weather: {forecast.tomorrow.weather_desc}")
|
|
85
|
-
print(f" Sunrise: {forecast.tomorrow.sunrise}, Sunset: {forecast.tomorrow.sunset}")
|
|
86
|
-
|
|
87
|
-
# Extended forecast (next 3 days)
|
|
88
|
-
if forecast.days and len(forecast.days) > 2:
|
|
89
|
-
print("\nExtended Forecast:")
|
|
90
|
-
for day in forecast.days[2:5]: # Days 3-5
|
|
91
|
-
print(f" {day.date_formatted}: {day.min_temp_c}°C - {day.max_temp_c}°C, {day.weather_desc}")
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## 🎨 ASCII Art Weather
|
|
95
|
-
|
|
96
|
-
Retrieve weather information as visually appealing ASCII art.
|
|
97
|
-
|
|
98
|
-
```python
|
|
99
|
-
from webscout.Extra import weather_ascii
|
|
100
|
-
|
|
101
|
-
# Get ASCII art weather
|
|
102
|
-
result = weather_ascii.get("Paris")
|
|
103
|
-
|
|
104
|
-
# Display the ASCII art weather
|
|
105
|
-
print(result.content)
|
|
106
|
-
|
|
107
|
-
# Get ASCII art with temperature in Fahrenheit
|
|
108
|
-
result_f = weather_ascii.get("New York", units="imperial")
|
|
109
|
-
print(result_f.content)
|
|
110
|
-
|
|
111
|
-
# Get ASCII art with a specific number of forecast days
|
|
112
|
-
result_days = weather_ascii.get("Berlin", days=3)
|
|
113
|
-
print(result_days.content)
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Example output:
|
|
117
|
-
```
|
|
118
|
-
Weather for Paris, France
|
|
119
|
-
|
|
120
|
-
\ / Clear
|
|
121
|
-
.-. +20°C
|
|
122
|
-
― ( ) ― ↗ 11 km/h
|
|
123
|
-
`-' 10 km
|
|
124
|
-
/ \ 0.0 mm
|
|
125
|
-
┌─────────────┐
|
|
126
|
-
┌───────────────────────┤ Wed 14 Apr ├───────────────────────┐
|
|
127
|
-
│ Morning └──────┬──────┘ Evening Night │
|
|
128
|
-
├──────────────────────────────┼──────────────────────────────┤
|
|
129
|
-
│ Cloudy │ Clear │
|
|
130
|
-
│ .--. +20..+22 °C │ \ / +15 °C │
|
|
131
|
-
│ .-( ). ↗ 11-13 km/h │ .-. ↗ 7-9 km/h │
|
|
132
|
-
│ (___.__)__) 10 km │ ― ( ) ― 10 km │
|
|
133
|
-
│ 0.0 mm | 0% │ `-' 0.0 mm | 0% │
|
|
134
|
-
└──────────────────────────────┴──────────────────────────────┘
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## 📊 Data Structure
|
|
138
|
-
|
|
139
|
-
The Weather Toolkit returns structured data objects with the following key components:
|
|
140
|
-
|
|
141
|
-
<details>
|
|
142
|
-
<summary><strong>Forecast Object Structure</strong></summary>
|
|
143
|
-
|
|
144
|
-
```python
|
|
145
|
-
forecast = weather.get("London")
|
|
146
|
-
|
|
147
|
-
# Main forecast object attributes
|
|
148
|
-
forecast.location # Location information (city, country, etc.)
|
|
149
|
-
forecast.summary # Brief summary of current weather
|
|
150
|
-
forecast.current_condition # Current weather conditions
|
|
151
|
-
forecast.today # Today's forecast
|
|
152
|
-
forecast.tomorrow # Tomorrow's forecast
|
|
153
|
-
forecast.days # List of daily forecasts (including today and tomorrow)
|
|
154
|
-
|
|
155
|
-
# Current condition attributes
|
|
156
|
-
current = forecast.current_condition
|
|
157
|
-
current.temp_c # Temperature in Celsius
|
|
158
|
-
current.temp_f # Temperature in Fahrenheit
|
|
159
|
-
current.feels_like_c # "Feels like" temperature in Celsius
|
|
160
|
-
current.feels_like_f # "Feels like" temperature in Fahrenheit
|
|
161
|
-
current.wind_speed_kmph # Wind speed in km/h
|
|
162
|
-
current.wind_speed_mph # Wind speed in mph
|
|
163
|
-
current.wind_direction # Wind direction (e.g., "NW")
|
|
164
|
-
current.humidity # Humidity percentage
|
|
165
|
-
current.pressure # Atmospheric pressure in millibars
|
|
166
|
-
current.visibility # Visibility in kilometers
|
|
167
|
-
current.weather_desc # Weather description (e.g., "Partly cloudy")
|
|
168
|
-
current.weather_code # Weather code for icon mapping
|
|
169
|
-
|
|
170
|
-
# Daily forecast attributes
|
|
171
|
-
day = forecast.today # or any day from forecast.days
|
|
172
|
-
day.date # Date (YYYY-MM-DD)
|
|
173
|
-
day.date_formatted # Formatted date (e.g., "Wed 14 Apr")
|
|
174
|
-
day.max_temp_c # Maximum temperature in Celsius
|
|
175
|
-
day.min_temp_c # Minimum temperature in Celsius
|
|
176
|
-
day.max_temp_f # Maximum temperature in Fahrenheit
|
|
177
|
-
day.min_temp_f # Minimum temperature in Fahrenheit
|
|
178
|
-
day.sunrise # Sunrise time
|
|
179
|
-
day.sunset # Sunset time
|
|
180
|
-
day.weather_desc # Weather description
|
|
181
|
-
day.weather_code # Weather code
|
|
182
|
-
day.hourly # List of hourly forecasts for this day
|
|
183
|
-
```
|
|
184
|
-
</details>
|
|
185
|
-
|
|
186
|
-
<details>
|
|
187
|
-
<summary><strong>ASCII Weather Object Structure</strong></summary>
|
|
188
|
-
|
|
189
|
-
```python
|
|
190
|
-
result = weather_ascii.get("Paris")
|
|
191
|
-
|
|
192
|
-
# ASCII result attributes
|
|
193
|
-
result.content # The full ASCII art weather display
|
|
194
|
-
result.location # Location information
|
|
195
|
-
result.temperature # Current temperature
|
|
196
|
-
result.conditions # Current weather conditions
|
|
197
|
-
result.forecast_days # Number of forecast days included
|
|
198
|
-
```
|
|
199
|
-
</details>
|
|
200
|
-
|
|
201
|
-
## ⚙️ Advanced Usage
|
|
202
|
-
|
|
203
|
-
<details>
|
|
204
|
-
<summary><strong>Custom Location Formats</strong></summary>
|
|
205
|
-
|
|
206
|
-
The Weather Toolkit supports various location formats:
|
|
207
|
-
|
|
208
|
-
```python
|
|
209
|
-
# City name
|
|
210
|
-
weather.get("London")
|
|
211
|
-
|
|
212
|
-
# City and country
|
|
213
|
-
weather.get("Paris, France")
|
|
214
|
-
|
|
215
|
-
# ZIP/Postal code (US)
|
|
216
|
-
weather.get("10001") # New York, NY
|
|
217
|
-
|
|
218
|
-
# Latitude and Longitude
|
|
219
|
-
weather.get("40.7128,-74.0060") # New York coordinates
|
|
220
|
-
```
|
|
221
|
-
</details>
|
|
222
|
-
|
|
223
|
-
<details>
|
|
224
|
-
<summary><strong>Temperature Units</strong></summary>
|
|
225
|
-
|
|
226
|
-
Control temperature units in ASCII weather display:
|
|
227
|
-
|
|
228
|
-
```python
|
|
229
|
-
# Default (metric - Celsius)
|
|
230
|
-
weather_ascii.get("Tokyo")
|
|
231
|
-
|
|
232
|
-
# Imperial (Fahrenheit)
|
|
233
|
-
weather_ascii.get("New York", units="imperial")
|
|
234
|
-
|
|
235
|
-
# Metric (Celsius)
|
|
236
|
-
weather_ascii.get("Berlin", units="metric")
|
|
237
|
-
```
|
|
238
|
-
</details>
|
|
239
|
-
|
|
240
|
-
<details>
|
|
241
|
-
<summary><strong>Forecast Days</strong></summary>
|
|
242
|
-
|
|
243
|
-
Control the number of forecast days in ASCII weather display:
|
|
244
|
-
|
|
245
|
-
```python
|
|
246
|
-
# Default (1 day)
|
|
247
|
-
weather_ascii.get("Sydney")
|
|
248
|
-
|
|
249
|
-
# 3-day forecast
|
|
250
|
-
weather_ascii.get("Rio de Janeiro", days=3)
|
|
251
|
-
|
|
252
|
-
# 5-day forecast (maximum)
|
|
253
|
-
weather_ascii.get("Moscow", days=5)
|
|
254
|
-
```
|
|
255
|
-
</details>
|
|
256
|
-
|
|
257
|
-
<details>
|
|
258
|
-
<summary><strong>Error Handling</strong></summary>
|
|
259
|
-
|
|
260
|
-
Implement proper error handling for robust applications:
|
|
261
|
-
|
|
262
|
-
```python
|
|
263
|
-
from webscout.Extra import weather
|
|
264
|
-
from webscout.exceptions import APIError
|
|
265
|
-
|
|
266
|
-
try:
|
|
267
|
-
forecast = weather.get("London")
|
|
268
|
-
print(f"Current temperature: {forecast.current_condition.temp_c}°C")
|
|
269
|
-
except APIError as e:
|
|
270
|
-
print(f"API Error: {e}")
|
|
271
|
-
except Exception as e:
|
|
272
|
-
print(f"An error occurred: {e}")
|
|
273
|
-
```
|
|
274
|
-
</details>
|
|
275
|
-
|
|
276
|
-
<div align="center">
|
|
277
|
-
<p>
|
|
278
|
-
<a href="https://github.com/OEvortex/Webscout"><img alt="GitHub Repository" src="https://img.shields.io/badge/GitHub-Repository-181717?style=for-the-badge&logo=github&logoColor=white"></a>
|
|
279
|
-
<a href="https://t.me/PyscoutAI"><img alt="Telegram Group" src="https://img.shields.io/badge/Telegram%20Group-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white"></a>
|
|
280
|
-
</p>
|
|
281
|
-
</div>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|