webscout 8.2.8__py3-none-any.whl → 8.2.9__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.
- webscout/AIauto.py +32 -14
- webscout/AIbase.py +96 -37
- webscout/AIutel.py +491 -87
- webscout/Bard.py +441 -323
- webscout/Extra/GitToolkit/__init__.py +10 -10
- webscout/Extra/YTToolkit/ytapi/video.py +232 -232
- webscout/Litlogger/README.md +10 -0
- webscout/Litlogger/__init__.py +7 -59
- webscout/Litlogger/formats.py +4 -0
- webscout/Litlogger/handlers.py +103 -0
- webscout/Litlogger/levels.py +13 -0
- webscout/Litlogger/logger.py +92 -0
- webscout/Provider/AISEARCH/Perplexity.py +332 -358
- webscout/Provider/AISEARCH/felo_search.py +9 -35
- webscout/Provider/AISEARCH/genspark_search.py +30 -56
- webscout/Provider/AISEARCH/hika_search.py +4 -16
- webscout/Provider/AISEARCH/iask_search.py +410 -436
- webscout/Provider/AISEARCH/monica_search.py +4 -30
- webscout/Provider/AISEARCH/scira_search.py +6 -32
- webscout/Provider/AISEARCH/webpilotai_search.py +38 -64
- webscout/Provider/Blackboxai.py +153 -35
- webscout/Provider/Deepinfra.py +339 -339
- webscout/Provider/ExaChat.py +358 -358
- webscout/Provider/Gemini.py +169 -169
- webscout/Provider/GithubChat.py +1 -2
- webscout/Provider/Glider.py +3 -3
- webscout/Provider/HeckAI.py +171 -81
- webscout/Provider/OPENAI/BLACKBOXAI.py +766 -735
- webscout/Provider/OPENAI/Cloudflare.py +7 -7
- webscout/Provider/OPENAI/FreeGemini.py +6 -5
- webscout/Provider/OPENAI/NEMOTRON.py +8 -20
- webscout/Provider/OPENAI/Qwen3.py +283 -0
- webscout/Provider/OPENAI/README.md +952 -1253
- webscout/Provider/OPENAI/TwoAI.py +357 -0
- webscout/Provider/OPENAI/__init__.py +5 -1
- webscout/Provider/OPENAI/ai4chat.py +40 -40
- webscout/Provider/OPENAI/api.py +808 -649
- webscout/Provider/OPENAI/c4ai.py +3 -3
- webscout/Provider/OPENAI/chatgpt.py +555 -555
- webscout/Provider/OPENAI/chatgptclone.py +493 -487
- webscout/Provider/OPENAI/chatsandbox.py +4 -3
- webscout/Provider/OPENAI/copilot.py +242 -0
- webscout/Provider/OPENAI/deepinfra.py +5 -2
- webscout/Provider/OPENAI/e2b.py +63 -5
- webscout/Provider/OPENAI/exaai.py +416 -410
- webscout/Provider/OPENAI/exachat.py +444 -443
- webscout/Provider/OPENAI/freeaichat.py +2 -2
- webscout/Provider/OPENAI/glider.py +5 -2
- webscout/Provider/OPENAI/groq.py +5 -2
- webscout/Provider/OPENAI/heckai.py +308 -307
- webscout/Provider/OPENAI/mcpcore.py +8 -2
- webscout/Provider/OPENAI/multichat.py +4 -4
- webscout/Provider/OPENAI/netwrck.py +6 -5
- webscout/Provider/OPENAI/oivscode.py +287 -0
- webscout/Provider/OPENAI/opkfc.py +496 -496
- webscout/Provider/OPENAI/pydantic_imports.py +172 -0
- webscout/Provider/OPENAI/scirachat.py +15 -9
- webscout/Provider/OPENAI/sonus.py +304 -303
- webscout/Provider/OPENAI/standardinput.py +433 -433
- webscout/Provider/OPENAI/textpollinations.py +4 -4
- webscout/Provider/OPENAI/toolbaz.py +413 -413
- webscout/Provider/OPENAI/typefully.py +3 -3
- webscout/Provider/OPENAI/typegpt.py +11 -5
- webscout/Provider/OPENAI/uncovrAI.py +463 -462
- webscout/Provider/OPENAI/utils.py +90 -79
- webscout/Provider/OPENAI/venice.py +431 -425
- webscout/Provider/OPENAI/wisecat.py +387 -381
- webscout/Provider/OPENAI/writecream.py +3 -3
- webscout/Provider/OPENAI/x0gpt.py +365 -378
- webscout/Provider/OPENAI/yep.py +39 -13
- webscout/Provider/TTI/README.md +55 -101
- webscout/Provider/TTI/__init__.py +4 -9
- webscout/Provider/TTI/aiarta.py +365 -0
- webscout/Provider/TTI/artbit.py +0 -0
- webscout/Provider/TTI/base.py +64 -0
- webscout/Provider/TTI/fastflux.py +200 -0
- webscout/Provider/TTI/magicstudio.py +201 -0
- webscout/Provider/TTI/piclumen.py +203 -0
- webscout/Provider/TTI/pixelmuse.py +225 -0
- webscout/Provider/TTI/pollinations.py +221 -0
- webscout/Provider/TTI/utils.py +11 -0
- webscout/Provider/TTS/__init__.py +2 -1
- webscout/Provider/TTS/base.py +159 -159
- webscout/Provider/TTS/openai_fm.py +129 -0
- webscout/Provider/TextPollinationsAI.py +308 -308
- webscout/Provider/TwoAI.py +239 -44
- webscout/Provider/UNFINISHED/Youchat.py +330 -330
- webscout/Provider/UNFINISHED/puterjs.py +635 -0
- webscout/Provider/UNFINISHED/test_lmarena.py +119 -119
- webscout/Provider/Writecream.py +246 -246
- webscout/Provider/__init__.py +2 -0
- webscout/Provider/ai4chat.py +33 -8
- webscout/Provider/koala.py +169 -169
- webscout/Provider/oivscode.py +309 -0
- webscout/Provider/samurai.py +3 -2
- webscout/Provider/typegpt.py +3 -3
- webscout/Provider/uncovr.py +368 -368
- webscout/client.py +70 -0
- webscout/litprinter/__init__.py +58 -58
- webscout/optimizers.py +419 -419
- webscout/scout/README.md +3 -1
- webscout/scout/core/crawler.py +134 -64
- webscout/scout/core/scout.py +148 -109
- webscout/scout/element.py +106 -88
- webscout/swiftcli/Readme.md +323 -323
- webscout/swiftcli/plugins/manager.py +9 -2
- webscout/version.py +1 -1
- webscout/zeroart/__init__.py +134 -134
- webscout/zeroart/effects.py +100 -100
- webscout/zeroart/fonts.py +1238 -1238
- {webscout-8.2.8.dist-info → webscout-8.2.9.dist-info}/METADATA +159 -35
- {webscout-8.2.8.dist-info → webscout-8.2.9.dist-info}/RECORD +116 -161
- {webscout-8.2.8.dist-info → webscout-8.2.9.dist-info}/WHEEL +1 -1
- {webscout-8.2.8.dist-info → webscout-8.2.9.dist-info}/entry_points.txt +1 -0
- webscout/Litlogger/Readme.md +0 -175
- webscout/Litlogger/core/__init__.py +0 -6
- webscout/Litlogger/core/level.py +0 -23
- webscout/Litlogger/core/logger.py +0 -165
- webscout/Litlogger/handlers/__init__.py +0 -12
- webscout/Litlogger/handlers/console.py +0 -33
- webscout/Litlogger/handlers/file.py +0 -143
- webscout/Litlogger/handlers/network.py +0 -173
- webscout/Litlogger/styles/__init__.py +0 -7
- webscout/Litlogger/styles/colors.py +0 -249
- webscout/Litlogger/styles/formats.py +0 -458
- webscout/Litlogger/styles/text.py +0 -87
- webscout/Litlogger/utils/__init__.py +0 -6
- webscout/Litlogger/utils/detectors.py +0 -153
- webscout/Litlogger/utils/formatters.py +0 -200
- webscout/Provider/TTI/AiForce/README.md +0 -159
- webscout/Provider/TTI/AiForce/__init__.py +0 -22
- webscout/Provider/TTI/AiForce/async_aiforce.py +0 -224
- webscout/Provider/TTI/AiForce/sync_aiforce.py +0 -245
- webscout/Provider/TTI/FreeAIPlayground/README.md +0 -99
- webscout/Provider/TTI/FreeAIPlayground/__init__.py +0 -9
- webscout/Provider/TTI/FreeAIPlayground/async_freeaiplayground.py +0 -181
- webscout/Provider/TTI/FreeAIPlayground/sync_freeaiplayground.py +0 -180
- webscout/Provider/TTI/ImgSys/README.md +0 -174
- webscout/Provider/TTI/ImgSys/__init__.py +0 -23
- webscout/Provider/TTI/ImgSys/async_imgsys.py +0 -202
- webscout/Provider/TTI/ImgSys/sync_imgsys.py +0 -195
- webscout/Provider/TTI/MagicStudio/README.md +0 -101
- webscout/Provider/TTI/MagicStudio/__init__.py +0 -2
- webscout/Provider/TTI/MagicStudio/async_magicstudio.py +0 -111
- webscout/Provider/TTI/MagicStudio/sync_magicstudio.py +0 -109
- webscout/Provider/TTI/Nexra/README.md +0 -155
- webscout/Provider/TTI/Nexra/__init__.py +0 -22
- webscout/Provider/TTI/Nexra/async_nexra.py +0 -286
- webscout/Provider/TTI/Nexra/sync_nexra.py +0 -258
- webscout/Provider/TTI/PollinationsAI/README.md +0 -146
- webscout/Provider/TTI/PollinationsAI/__init__.py +0 -23
- webscout/Provider/TTI/PollinationsAI/async_pollinations.py +0 -311
- webscout/Provider/TTI/PollinationsAI/sync_pollinations.py +0 -265
- webscout/Provider/TTI/aiarta/README.md +0 -134
- webscout/Provider/TTI/aiarta/__init__.py +0 -2
- webscout/Provider/TTI/aiarta/async_aiarta.py +0 -482
- webscout/Provider/TTI/aiarta/sync_aiarta.py +0 -440
- webscout/Provider/TTI/artbit/README.md +0 -100
- webscout/Provider/TTI/artbit/__init__.py +0 -22
- webscout/Provider/TTI/artbit/async_artbit.py +0 -155
- webscout/Provider/TTI/artbit/sync_artbit.py +0 -148
- webscout/Provider/TTI/fastflux/README.md +0 -129
- webscout/Provider/TTI/fastflux/__init__.py +0 -22
- webscout/Provider/TTI/fastflux/async_fastflux.py +0 -261
- webscout/Provider/TTI/fastflux/sync_fastflux.py +0 -252
- webscout/Provider/TTI/huggingface/README.md +0 -114
- webscout/Provider/TTI/huggingface/__init__.py +0 -22
- webscout/Provider/TTI/huggingface/async_huggingface.py +0 -199
- webscout/Provider/TTI/huggingface/sync_huggingface.py +0 -195
- webscout/Provider/TTI/piclumen/README.md +0 -161
- webscout/Provider/TTI/piclumen/__init__.py +0 -23
- webscout/Provider/TTI/piclumen/async_piclumen.py +0 -268
- webscout/Provider/TTI/piclumen/sync_piclumen.py +0 -233
- webscout/Provider/TTI/pixelmuse/README.md +0 -79
- webscout/Provider/TTI/pixelmuse/__init__.py +0 -4
- webscout/Provider/TTI/pixelmuse/async_pixelmuse.py +0 -249
- webscout/Provider/TTI/pixelmuse/sync_pixelmuse.py +0 -182
- webscout/Provider/TTI/talkai/README.md +0 -139
- webscout/Provider/TTI/talkai/__init__.py +0 -4
- webscout/Provider/TTI/talkai/async_talkai.py +0 -229
- webscout/Provider/TTI/talkai/sync_talkai.py +0 -207
- webscout/Provider/UNFINISHED/oivscode.py +0 -351
- {webscout-8.2.8.dist-info → webscout-8.2.9.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.2.8.dist-info → webscout-8.2.9.dist-info}/top_level.txt +0 -0
webscout/Litlogger/core/level.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
|
|
3
|
-
class LogLevel(Enum):
|
|
4
|
-
NOTSET = 0
|
|
5
|
-
DEBUG = 10
|
|
6
|
-
INFO = 20
|
|
7
|
-
WARNING = 30
|
|
8
|
-
ERROR = 40
|
|
9
|
-
CRITICAL = 50
|
|
10
|
-
|
|
11
|
-
@staticmethod
|
|
12
|
-
def get_level(level_str: str) -> 'LogLevel':
|
|
13
|
-
if not level_str:
|
|
14
|
-
return LogLevel.NOTSET
|
|
15
|
-
try:
|
|
16
|
-
return LogLevel[level_str.upper()]
|
|
17
|
-
except KeyError:
|
|
18
|
-
raise ValueError(f"Invalid log level: {level_str}")
|
|
19
|
-
|
|
20
|
-
def __lt__(self, other):
|
|
21
|
-
if isinstance(other, LogLevel):
|
|
22
|
-
return self.value < other.value
|
|
23
|
-
return NotImplemented
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import asyncio
|
|
3
|
-
import sys
|
|
4
|
-
import threading
|
|
5
|
-
import traceback
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from typing import List, Union
|
|
8
|
-
|
|
9
|
-
from ..core.level import LogLevel
|
|
10
|
-
from ..styles.formats import LogFormat
|
|
11
|
-
|
|
12
|
-
class Logger:
|
|
13
|
-
# Emoji mappings for different log levels
|
|
14
|
-
LEVEL_EMOJIS = {
|
|
15
|
-
LogLevel.DEBUG: "🔍",
|
|
16
|
-
LogLevel.INFO: "ℹ️",
|
|
17
|
-
LogLevel.WARNING: "⚠️",
|
|
18
|
-
LogLevel.ERROR: "❌",
|
|
19
|
-
LogLevel.CRITICAL: "🔥"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
def __init__(
|
|
23
|
-
self,
|
|
24
|
-
name: str = "LitLogger",
|
|
25
|
-
level: Union[str, LogLevel, None] = None,
|
|
26
|
-
format: Union[str, LogFormat] = LogFormat.MODERN_EMOJI,
|
|
27
|
-
handlers: List = None,
|
|
28
|
-
enable_colors: bool = True,
|
|
29
|
-
async_mode: bool = False,
|
|
30
|
-
show_thread: bool = True,
|
|
31
|
-
show_context: bool = True
|
|
32
|
-
):
|
|
33
|
-
self.name = name
|
|
34
|
-
self.level = LogLevel.NOTSET if level is None else (
|
|
35
|
-
LogLevel.get_level(level) if isinstance(level, str) else level
|
|
36
|
-
)
|
|
37
|
-
self.format = format
|
|
38
|
-
self.enable_colors = enable_colors
|
|
39
|
-
self.async_mode = async_mode
|
|
40
|
-
self.show_thread = show_thread
|
|
41
|
-
self.show_context = show_context
|
|
42
|
-
self._context_data = {}
|
|
43
|
-
|
|
44
|
-
# Initialize with default console handler if none provided
|
|
45
|
-
if handlers is None:
|
|
46
|
-
from ..handlers.console import ConsoleHandler
|
|
47
|
-
self.handlers = [ConsoleHandler(level=self.level)]
|
|
48
|
-
else:
|
|
49
|
-
self.handlers = handlers
|
|
50
|
-
|
|
51
|
-
def _format_message(self, level: LogLevel, message: str, **kwargs) -> str:
|
|
52
|
-
now = datetime.now()
|
|
53
|
-
emoji = self.LEVEL_EMOJIS.get(level, "") if self.enable_colors else ""
|
|
54
|
-
|
|
55
|
-
log_data = {
|
|
56
|
-
"timestamp": now.strftime("%H:%M:%S"),
|
|
57
|
-
"level": level.name,
|
|
58
|
-
"name": self.name,
|
|
59
|
-
"message": str(message),
|
|
60
|
-
"emoji": emoji,
|
|
61
|
-
"thread": threading.current_thread().name if self.show_thread else "",
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Add context data
|
|
65
|
-
if self.show_context and self._context_data:
|
|
66
|
-
log_data.update(self._context_data)
|
|
67
|
-
|
|
68
|
-
# Add extra kwargs
|
|
69
|
-
log_data.update(kwargs)
|
|
70
|
-
|
|
71
|
-
# Format exception if present
|
|
72
|
-
if 'exc_info' in kwargs:
|
|
73
|
-
exc_info = kwargs['exc_info']
|
|
74
|
-
if exc_info:
|
|
75
|
-
exception_text = ''.join(traceback.format_exception(*exc_info))
|
|
76
|
-
log_data['message'] = f"{message}\n{exception_text}"
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
base_message = f"{emoji} [{log_data['timestamp']}] {level.name} {log_data['message']}"
|
|
80
|
-
return base_message
|
|
81
|
-
except Exception as e:
|
|
82
|
-
return f"[{log_data['timestamp']}] {level.name}: {message}"
|
|
83
|
-
|
|
84
|
-
def _log(self, level: LogLevel, message: str, **kwargs):
|
|
85
|
-
if self.async_mode:
|
|
86
|
-
loop = asyncio.get_event_loop()
|
|
87
|
-
if loop.is_running():
|
|
88
|
-
return asyncio.create_task(self._async_log(level, message, **kwargs))
|
|
89
|
-
else:
|
|
90
|
-
return loop.run_until_complete(self._async_log(level, message, **kwargs))
|
|
91
|
-
return self._sync_log(level, message, **kwargs)
|
|
92
|
-
|
|
93
|
-
def debug(self, message: str, **kwargs):
|
|
94
|
-
self._log(LogLevel.DEBUG, message, **kwargs)
|
|
95
|
-
|
|
96
|
-
def info(self, message: str, **kwargs):
|
|
97
|
-
self._log(LogLevel.INFO, message, **kwargs)
|
|
98
|
-
|
|
99
|
-
def warning(self, message: str, **kwargs):
|
|
100
|
-
self._log(LogLevel.WARNING, message, **kwargs)
|
|
101
|
-
|
|
102
|
-
def error(self, message: str, **kwargs):
|
|
103
|
-
self._log(LogLevel.ERROR, message, **kwargs)
|
|
104
|
-
|
|
105
|
-
def critical(self, message: str, **kwargs):
|
|
106
|
-
self._log(LogLevel.CRITICAL, message, **kwargs)
|
|
107
|
-
|
|
108
|
-
def exception(self, message: str, exc_info=True, **kwargs):
|
|
109
|
-
"""
|
|
110
|
-
Log an exception with traceback.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
message: The message to log
|
|
114
|
-
exc_info: If True, adds exception info to the message. Can also be a tuple (type, value, traceback)
|
|
115
|
-
**kwargs: Additional key-value pairs to log
|
|
116
|
-
"""
|
|
117
|
-
if exc_info:
|
|
118
|
-
if not isinstance(exc_info, tuple):
|
|
119
|
-
exc_info = sys.exc_info()
|
|
120
|
-
kwargs['exc_info'] = exc_info
|
|
121
|
-
self.error(message, **kwargs)
|
|
122
|
-
|
|
123
|
-
def _sync_log(self, level: LogLevel, message: str, **kwargs):
|
|
124
|
-
if self._should_log(level):
|
|
125
|
-
formatted_message = self._format_message(level, message, **kwargs)
|
|
126
|
-
for handler in self.handlers:
|
|
127
|
-
if handler.level == LogLevel.NOTSET or level.value >= handler.level.value:
|
|
128
|
-
handler.emit(formatted_message, level)
|
|
129
|
-
|
|
130
|
-
async def _async_log(self, level: LogLevel, message: str, **kwargs):
|
|
131
|
-
if self._should_log(level):
|
|
132
|
-
formatted_message = self._format_message(level, message, **kwargs)
|
|
133
|
-
tasks = []
|
|
134
|
-
for handler in self.handlers:
|
|
135
|
-
if handler.level == LogLevel.NOTSET or level.value >= handler.level.value:
|
|
136
|
-
if hasattr(handler, 'async_emit'):
|
|
137
|
-
tasks.append(handler.async_emit(formatted_message, level))
|
|
138
|
-
else:
|
|
139
|
-
tasks.append(asyncio.to_thread(handler.emit, formatted_message, level))
|
|
140
|
-
await asyncio.gather(*tasks)
|
|
141
|
-
|
|
142
|
-
def _should_log(self, level: LogLevel) -> bool:
|
|
143
|
-
return self.level == LogLevel.NOTSET or level.value >= self.level.value
|
|
144
|
-
|
|
145
|
-
def set_context(self, **kwargs):
|
|
146
|
-
self._context_data.update(kwargs)
|
|
147
|
-
|
|
148
|
-
def clear_context(self):
|
|
149
|
-
self._context_data.clear()
|
|
150
|
-
|
|
151
|
-
def set_style(self, style: str):
|
|
152
|
-
"""Set logger style format."""
|
|
153
|
-
if style in LogFormat.TEMPLATES:
|
|
154
|
-
self.format = LogFormat.TEMPLATES[style]
|
|
155
|
-
else:
|
|
156
|
-
raise ValueError(f"Unknown style: {style}")
|
|
157
|
-
|
|
158
|
-
def __enter__(self):
|
|
159
|
-
return self
|
|
160
|
-
|
|
161
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
162
|
-
if exc_type is not None:
|
|
163
|
-
self.error(f"Context exited with error: {exc_val}")
|
|
164
|
-
return False
|
|
165
|
-
return True
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"""Log output handlers for different destinations."""
|
|
2
|
-
|
|
3
|
-
from .console import ConsoleHandler, ErrorConsoleHandler
|
|
4
|
-
from .file import FileHandler
|
|
5
|
-
from .network import NetworkHandler
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
"ConsoleHandler",
|
|
9
|
-
"ErrorConsoleHandler",
|
|
10
|
-
"FileHandler",
|
|
11
|
-
"NetworkHandler"
|
|
12
|
-
]
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from ..core.level import LogLevel
|
|
3
|
-
from ..styles.colors import LogColors
|
|
4
|
-
|
|
5
|
-
class ConsoleHandler:
|
|
6
|
-
def __init__(self, level: LogLevel = LogLevel.DEBUG, stream=sys.stdout):
|
|
7
|
-
self.level = level
|
|
8
|
-
self.stream = stream
|
|
9
|
-
self.colors = LogColors()
|
|
10
|
-
|
|
11
|
-
def emit(self, message: str, level: LogLevel):
|
|
12
|
-
"""Write the colored message to the console."""
|
|
13
|
-
try:
|
|
14
|
-
# Apply color based on log level
|
|
15
|
-
color = LogColors.LEVEL_COLORS.get(level, LogColors.RESET)
|
|
16
|
-
colored_message = f"{color}{message}{LogColors.RESET}"
|
|
17
|
-
print(colored_message, file=self.stream, flush=True)
|
|
18
|
-
except Exception as e:
|
|
19
|
-
# Fallback to plain printing if coloring fails
|
|
20
|
-
print(message, file=self.stream, flush=True)
|
|
21
|
-
|
|
22
|
-
async def async_emit(self, message: str, level: LogLevel):
|
|
23
|
-
"""
|
|
24
|
-
Asynchronously write log message to console.
|
|
25
|
-
Just calls emit() since console output is generally fast enough.
|
|
26
|
-
"""
|
|
27
|
-
self.emit(message, level)
|
|
28
|
-
|
|
29
|
-
class ErrorConsoleHandler(ConsoleHandler):
|
|
30
|
-
"""Specialized handler that writes to stderr."""
|
|
31
|
-
|
|
32
|
-
def __init__(self, level: LogLevel = LogLevel.ERROR):
|
|
33
|
-
super().__init__(stream=sys.stderr, level=level)
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import time
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Union
|
|
6
|
-
from ..core.level import LogLevel
|
|
7
|
-
|
|
8
|
-
class FileHandler:
|
|
9
|
-
"""Handler for outputting log messages to a file with optional rotation."""
|
|
10
|
-
|
|
11
|
-
def __init__(
|
|
12
|
-
self,
|
|
13
|
-
filename: Union[str, Path],
|
|
14
|
-
mode: str = "a",
|
|
15
|
-
encoding: str = "utf-8",
|
|
16
|
-
level: LogLevel = LogLevel.DEBUG,
|
|
17
|
-
max_bytes: int = 0,
|
|
18
|
-
backup_count: int = 0,
|
|
19
|
-
rotate_on_time: bool = False,
|
|
20
|
-
time_interval: str = "D" # D=daily, H=hourly, M=monthly
|
|
21
|
-
):
|
|
22
|
-
"""
|
|
23
|
-
Initialize file handler with rotation options.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
filename: Log file path
|
|
27
|
-
mode: File open mode ('a' for append, 'w' for write)
|
|
28
|
-
encoding: File encoding
|
|
29
|
-
level: Minimum log level to output
|
|
30
|
-
max_bytes: Max file size before rotation (0 = no size limit)
|
|
31
|
-
backup_count: Number of backup files to keep (0 = no backups)
|
|
32
|
-
rotate_on_time: Enable time-based rotation
|
|
33
|
-
time_interval: Rotation interval ('D'=daily, 'H'=hourly, 'M'=monthly)
|
|
34
|
-
"""
|
|
35
|
-
self.filename = Path(filename)
|
|
36
|
-
self.mode = mode
|
|
37
|
-
self.encoding = encoding
|
|
38
|
-
self.level = level
|
|
39
|
-
self.max_bytes = max_bytes
|
|
40
|
-
self.backup_count = backup_count
|
|
41
|
-
self.rotate_on_time = rotate_on_time
|
|
42
|
-
self.time_interval = time_interval.upper()
|
|
43
|
-
|
|
44
|
-
if self.time_interval not in ["D", "H", "M"]:
|
|
45
|
-
raise ValueError("time_interval must be 'D', 'H', or 'M'")
|
|
46
|
-
|
|
47
|
-
self._file = None
|
|
48
|
-
self._current_size = 0
|
|
49
|
-
self._last_rollover_time = time.time()
|
|
50
|
-
|
|
51
|
-
# Create directory if it doesn't exist
|
|
52
|
-
self.filename.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
-
|
|
54
|
-
# Open the file
|
|
55
|
-
self._open()
|
|
56
|
-
|
|
57
|
-
def _open(self):
|
|
58
|
-
"""Open or reopen the log file."""
|
|
59
|
-
if self._file:
|
|
60
|
-
self._file.close()
|
|
61
|
-
|
|
62
|
-
self._file = open(
|
|
63
|
-
self.filename,
|
|
64
|
-
mode=self.mode,
|
|
65
|
-
encoding=self.encoding
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
self._current_size = self._file.tell()
|
|
69
|
-
if self.mode == "a":
|
|
70
|
-
self._current_size = self.filename.stat().st_size
|
|
71
|
-
|
|
72
|
-
def _should_rollover(self) -> bool:
|
|
73
|
-
"""Check if file should be rolled over based on size or time."""
|
|
74
|
-
if self.max_bytes > 0 and self._current_size >= self.max_bytes:
|
|
75
|
-
return True
|
|
76
|
-
|
|
77
|
-
if self.rotate_on_time:
|
|
78
|
-
current_time = time.time()
|
|
79
|
-
if self.time_interval == "H":
|
|
80
|
-
interval = 3600 # 1 hour
|
|
81
|
-
elif self.time_interval == "D":
|
|
82
|
-
interval = 86400 # 1 day
|
|
83
|
-
else: # Monthly
|
|
84
|
-
now = datetime.now()
|
|
85
|
-
if now.month == datetime.fromtimestamp(self._last_rollover_time).month:
|
|
86
|
-
return False
|
|
87
|
-
return True
|
|
88
|
-
|
|
89
|
-
if current_time - self._last_rollover_time >= interval:
|
|
90
|
-
return True
|
|
91
|
-
|
|
92
|
-
return False
|
|
93
|
-
|
|
94
|
-
def _do_rollover(self):
|
|
95
|
-
"""Perform log file rotation."""
|
|
96
|
-
if self._file:
|
|
97
|
-
self._file.close()
|
|
98
|
-
self._file = None
|
|
99
|
-
|
|
100
|
-
if self.backup_count > 0:
|
|
101
|
-
# Shift existing backup files
|
|
102
|
-
for i in range(self.backup_count - 1, 0, -1):
|
|
103
|
-
sfn = f"{self.filename}.{i}"
|
|
104
|
-
dfn = f"{self.filename}.{i + 1}"
|
|
105
|
-
if os.path.exists(sfn):
|
|
106
|
-
if os.path.exists(dfn):
|
|
107
|
-
os.remove(dfn)
|
|
108
|
-
os.rename(sfn, dfn)
|
|
109
|
-
|
|
110
|
-
dfn = f"{self.filename}.1"
|
|
111
|
-
if os.path.exists(dfn):
|
|
112
|
-
os.remove(dfn)
|
|
113
|
-
os.rename(self.filename, dfn)
|
|
114
|
-
|
|
115
|
-
self._open()
|
|
116
|
-
self._last_rollover_time = time.time()
|
|
117
|
-
|
|
118
|
-
def emit(self, message: str, level: LogLevel):
|
|
119
|
-
"""Write log message to file if level is sufficient."""
|
|
120
|
-
if level.value >= self.level.value:
|
|
121
|
-
try:
|
|
122
|
-
if self._should_rollover():
|
|
123
|
-
self._do_rollover()
|
|
124
|
-
|
|
125
|
-
self._file.write(message + "\n")
|
|
126
|
-
self._file.flush()
|
|
127
|
-
self._current_size = self._file.tell()
|
|
128
|
-
|
|
129
|
-
except Exception as e:
|
|
130
|
-
# Fallback to console on error
|
|
131
|
-
import sys
|
|
132
|
-
sys.stderr.write(f"Error in FileHandler: {e}\n")
|
|
133
|
-
sys.stderr.write(message + "\n")
|
|
134
|
-
|
|
135
|
-
async def async_emit(self, message: str, level: LogLevel):
|
|
136
|
-
"""Asynchronously write log message to file."""
|
|
137
|
-
self.emit(message, level)
|
|
138
|
-
|
|
139
|
-
def close(self):
|
|
140
|
-
"""Close the log file."""
|
|
141
|
-
if self._file:
|
|
142
|
-
self._file.close()
|
|
143
|
-
self._file = None
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import socket
|
|
4
|
-
from typing import Optional, Dict, Any
|
|
5
|
-
import aiohttp
|
|
6
|
-
from ..core.level import LogLevel
|
|
7
|
-
|
|
8
|
-
class NetworkHandler:
|
|
9
|
-
"""Handler for sending log messages to a remote server."""
|
|
10
|
-
|
|
11
|
-
def __init__(
|
|
12
|
-
self,
|
|
13
|
-
host: str,
|
|
14
|
-
port: int,
|
|
15
|
-
protocol: str = "http",
|
|
16
|
-
endpoint: str = "/logs",
|
|
17
|
-
method: str = "POST",
|
|
18
|
-
headers: Optional[Dict[str, str]] = None,
|
|
19
|
-
timeout: float = 5.0,
|
|
20
|
-
level: LogLevel = LogLevel.DEBUG,
|
|
21
|
-
batch_size: int = 0,
|
|
22
|
-
retry_count: int = 3,
|
|
23
|
-
retry_delay: float = 1.0,
|
|
24
|
-
custom_fields: Optional[Dict[str, Any]] = None
|
|
25
|
-
):
|
|
26
|
-
"""
|
|
27
|
-
Initialize network handler.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
host: Remote server hostname/IP
|
|
31
|
-
port: Remote server port
|
|
32
|
-
protocol: 'http', 'https', or 'tcp'
|
|
33
|
-
endpoint: Server endpoint for HTTP/HTTPS
|
|
34
|
-
method: HTTP method to use
|
|
35
|
-
headers: Optional HTTP headers
|
|
36
|
-
timeout: Request timeout in seconds
|
|
37
|
-
level: Minimum log level to send
|
|
38
|
-
batch_size: Number of logs to batch (0 = no batching)
|
|
39
|
-
retry_count: Number of retries on failure
|
|
40
|
-
retry_delay: Delay between retries in seconds
|
|
41
|
-
custom_fields: Additional fields to include in log data
|
|
42
|
-
"""
|
|
43
|
-
self.host = host
|
|
44
|
-
self.port = port
|
|
45
|
-
self.protocol = protocol.lower()
|
|
46
|
-
self.endpoint = endpoint
|
|
47
|
-
self.method = method.upper()
|
|
48
|
-
self.headers = headers or {}
|
|
49
|
-
self.timeout = timeout
|
|
50
|
-
self.level = level
|
|
51
|
-
self.batch_size = batch_size
|
|
52
|
-
self.retry_count = retry_count
|
|
53
|
-
self.retry_delay = retry_delay
|
|
54
|
-
self.custom_fields = custom_fields or {}
|
|
55
|
-
|
|
56
|
-
if self.protocol not in ["http", "https", "tcp"]:
|
|
57
|
-
raise ValueError("Protocol must be 'http', 'https' or 'tcp'")
|
|
58
|
-
|
|
59
|
-
self._batch = []
|
|
60
|
-
self._tcp_socket = None
|
|
61
|
-
self._session = None
|
|
62
|
-
|
|
63
|
-
async def _init_session(self):
|
|
64
|
-
"""Initialize HTTP session if needed."""
|
|
65
|
-
if self.protocol in ["http", "https"] and not self._session:
|
|
66
|
-
self._session = aiohttp.ClientSession(
|
|
67
|
-
timeout=aiohttp.ClientTimeout(total=self.timeout)
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
async def _send_http(self, data: Dict[str, Any]) -> bool:
|
|
71
|
-
"""Send log data via HTTP/HTTPS."""
|
|
72
|
-
await self._init_session()
|
|
73
|
-
|
|
74
|
-
url = f"{self.protocol}://{self.host}:{self.port}{self.endpoint}"
|
|
75
|
-
|
|
76
|
-
for attempt in range(self.retry_count + 1):
|
|
77
|
-
try:
|
|
78
|
-
async with self._session.request(
|
|
79
|
-
method=self.method,
|
|
80
|
-
url=url,
|
|
81
|
-
json=data,
|
|
82
|
-
headers=self.headers
|
|
83
|
-
) as response:
|
|
84
|
-
return response.status < 400
|
|
85
|
-
|
|
86
|
-
except Exception:
|
|
87
|
-
if attempt == self.retry_count:
|
|
88
|
-
return False
|
|
89
|
-
await asyncio.sleep(self.retry_delay)
|
|
90
|
-
|
|
91
|
-
async def _send_tcp(self, data: Dict[str, Any]) -> bool:
|
|
92
|
-
"""Send log data via TCP."""
|
|
93
|
-
message = json.dumps(data).encode() + b"\n"
|
|
94
|
-
|
|
95
|
-
for attempt in range(self.retry_count + 1):
|
|
96
|
-
try:
|
|
97
|
-
if not self._tcp_socket:
|
|
98
|
-
self._tcp_socket = socket.socket(
|
|
99
|
-
socket.AF_INET, socket.SOCK_STREAM
|
|
100
|
-
)
|
|
101
|
-
self._tcp_socket.settimeout(self.timeout)
|
|
102
|
-
self._tcp_socket.connect((self.host, self.port))
|
|
103
|
-
|
|
104
|
-
self._tcp_socket.sendall(message)
|
|
105
|
-
return True
|
|
106
|
-
|
|
107
|
-
except Exception:
|
|
108
|
-
if self._tcp_socket:
|
|
109
|
-
self._tcp_socket.close()
|
|
110
|
-
self._tcp_socket = None
|
|
111
|
-
|
|
112
|
-
if attempt == self.retry_count:
|
|
113
|
-
return False
|
|
114
|
-
|
|
115
|
-
await asyncio.sleep(self.retry_delay)
|
|
116
|
-
|
|
117
|
-
def emit(self, message: str, level: LogLevel):
|
|
118
|
-
"""
|
|
119
|
-
Synchronously send log message.
|
|
120
|
-
Not recommended - use async_emit instead.
|
|
121
|
-
"""
|
|
122
|
-
if level.value >= self.level.value:
|
|
123
|
-
loop = asyncio.get_event_loop()
|
|
124
|
-
loop.run_until_complete(self.async_emit(message, level))
|
|
125
|
-
|
|
126
|
-
async def async_emit(self, message: str, level: LogLevel):
|
|
127
|
-
"""Asynchronously send log message to remote server."""
|
|
128
|
-
# Fix: Allow all messages if level is NOTSET
|
|
129
|
-
if self.level == LogLevel.NOTSET or level.value >= self.level.value:
|
|
130
|
-
log_data = {
|
|
131
|
-
"message": message,
|
|
132
|
-
"level": level.name,
|
|
133
|
-
**self.custom_fields
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if self.batch_size > 0:
|
|
137
|
-
self._batch.append(log_data)
|
|
138
|
-
if len(self._batch) >= self.batch_size:
|
|
139
|
-
await self._send_batch()
|
|
140
|
-
else:
|
|
141
|
-
if self.protocol in ["http", "https"]:
|
|
142
|
-
await self._send_http(log_data)
|
|
143
|
-
else:
|
|
144
|
-
await self._send_tcp(log_data)
|
|
145
|
-
|
|
146
|
-
async def _send_batch(self):
|
|
147
|
-
"""Send batched log messages."""
|
|
148
|
-
if not self._batch:
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
batch_data = {"logs": self._batch}
|
|
152
|
-
success = False
|
|
153
|
-
|
|
154
|
-
if self.protocol in ["http", "https"]:
|
|
155
|
-
success = await self._send_http(batch_data)
|
|
156
|
-
else:
|
|
157
|
-
success = await self._send_tcp(batch_data)
|
|
158
|
-
|
|
159
|
-
if success:
|
|
160
|
-
self._batch.clear()
|
|
161
|
-
|
|
162
|
-
async def close(self):
|
|
163
|
-
"""Close network connections."""
|
|
164
|
-
if self._batch:
|
|
165
|
-
await self._send_batch()
|
|
166
|
-
|
|
167
|
-
if self._tcp_socket:
|
|
168
|
-
self._tcp_socket.close()
|
|
169
|
-
self._tcp_socket = None
|
|
170
|
-
|
|
171
|
-
if self._session:
|
|
172
|
-
await self._session.close()
|
|
173
|
-
self._session = None
|