webscout 7.0__py3-none-any.whl → 7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of webscout might be problematic. Click here for more details.
- webscout/AIauto.py +191 -191
- webscout/AIbase.py +122 -122
- webscout/AIutel.py +440 -440
- webscout/Bard.py +343 -161
- webscout/DWEBS.py +489 -492
- webscout/Extra/YTToolkit/YTdownloader.py +995 -995
- webscout/Extra/YTToolkit/__init__.py +2 -2
- webscout/Extra/YTToolkit/transcriber.py +476 -479
- webscout/Extra/YTToolkit/ytapi/channel.py +307 -307
- webscout/Extra/YTToolkit/ytapi/playlist.py +58 -58
- webscout/Extra/YTToolkit/ytapi/pool.py +7 -7
- webscout/Extra/YTToolkit/ytapi/utils.py +62 -62
- webscout/Extra/YTToolkit/ytapi/video.py +103 -103
- webscout/Extra/autocoder/__init__.py +9 -9
- webscout/Extra/autocoder/autocoder_utiles.py +199 -199
- webscout/Extra/autocoder/rawdog.py +5 -7
- webscout/Extra/autollama.py +230 -230
- webscout/Extra/gguf.py +3 -3
- webscout/Extra/weather.py +171 -171
- webscout/LLM.py +442 -442
- webscout/Litlogger/__init__.py +67 -681
- webscout/Litlogger/core/__init__.py +6 -0
- webscout/Litlogger/core/level.py +20 -0
- webscout/Litlogger/core/logger.py +123 -0
- webscout/Litlogger/handlers/__init__.py +12 -0
- webscout/Litlogger/handlers/console.py +50 -0
- webscout/Litlogger/handlers/file.py +143 -0
- webscout/Litlogger/handlers/network.py +174 -0
- webscout/Litlogger/styles/__init__.py +7 -0
- webscout/Litlogger/styles/colors.py +231 -0
- webscout/Litlogger/styles/formats.py +377 -0
- webscout/Litlogger/styles/text.py +87 -0
- webscout/Litlogger/utils/__init__.py +6 -0
- webscout/Litlogger/utils/detectors.py +154 -0
- webscout/Litlogger/utils/formatters.py +200 -0
- webscout/Provider/AISEARCH/DeepFind.py +250 -250
- webscout/Provider/Blackboxai.py +136 -137
- webscout/Provider/ChatGPTGratis.py +226 -0
- webscout/Provider/Cloudflare.py +91 -78
- webscout/Provider/DeepSeek.py +218 -0
- webscout/Provider/Deepinfra.py +59 -35
- webscout/Provider/Free2GPT.py +131 -124
- webscout/Provider/Gemini.py +100 -115
- webscout/Provider/Glider.py +74 -59
- webscout/Provider/Groq.py +30 -18
- webscout/Provider/Jadve.py +108 -77
- webscout/Provider/Llama3.py +117 -94
- webscout/Provider/Marcus.py +191 -137
- webscout/Provider/Netwrck.py +62 -50
- webscout/Provider/PI.py +79 -124
- webscout/Provider/PizzaGPT.py +129 -83
- webscout/Provider/QwenLM.py +311 -0
- webscout/Provider/TTI/AiForce/__init__.py +22 -22
- webscout/Provider/TTI/AiForce/async_aiforce.py +257 -257
- webscout/Provider/TTI/AiForce/sync_aiforce.py +242 -242
- webscout/Provider/TTI/Nexra/__init__.py +22 -22
- webscout/Provider/TTI/Nexra/async_nexra.py +286 -286
- webscout/Provider/TTI/Nexra/sync_nexra.py +258 -258
- webscout/Provider/TTI/PollinationsAI/__init__.py +23 -23
- webscout/Provider/TTI/PollinationsAI/async_pollinations.py +330 -330
- webscout/Provider/TTI/PollinationsAI/sync_pollinations.py +285 -285
- webscout/Provider/TTI/artbit/__init__.py +22 -22
- webscout/Provider/TTI/artbit/async_artbit.py +184 -184
- webscout/Provider/TTI/artbit/sync_artbit.py +176 -176
- webscout/Provider/TTI/blackbox/__init__.py +4 -4
- webscout/Provider/TTI/blackbox/async_blackbox.py +212 -212
- webscout/Provider/TTI/blackbox/sync_blackbox.py +199 -199
- webscout/Provider/TTI/deepinfra/__init__.py +4 -4
- webscout/Provider/TTI/deepinfra/async_deepinfra.py +227 -227
- webscout/Provider/TTI/deepinfra/sync_deepinfra.py +199 -199
- webscout/Provider/TTI/huggingface/__init__.py +22 -22
- webscout/Provider/TTI/huggingface/async_huggingface.py +199 -199
- webscout/Provider/TTI/huggingface/sync_huggingface.py +195 -195
- webscout/Provider/TTI/imgninza/__init__.py +4 -4
- webscout/Provider/TTI/imgninza/async_ninza.py +214 -214
- webscout/Provider/TTI/imgninza/sync_ninza.py +209 -209
- webscout/Provider/TTI/talkai/__init__.py +4 -4
- webscout/Provider/TTI/talkai/async_talkai.py +229 -229
- webscout/Provider/TTI/talkai/sync_talkai.py +207 -207
- webscout/Provider/TTS/deepgram.py +182 -182
- webscout/Provider/TTS/elevenlabs.py +136 -136
- webscout/Provider/TTS/gesserit.py +150 -150
- webscout/Provider/TTS/murfai.py +138 -138
- webscout/Provider/TTS/parler.py +133 -134
- webscout/Provider/TTS/streamElements.py +360 -360
- webscout/Provider/TTS/utils.py +280 -280
- webscout/Provider/TTS/voicepod.py +116 -116
- webscout/Provider/TextPollinationsAI.py +74 -47
- webscout/Provider/WiseCat.py +193 -0
- webscout/Provider/__init__.py +144 -136
- webscout/Provider/cerebras.py +242 -227
- webscout/Provider/chatglm.py +204 -204
- webscout/Provider/dgaf.py +67 -39
- webscout/Provider/gaurish.py +105 -66
- webscout/Provider/geminiapi.py +208 -208
- webscout/Provider/granite.py +223 -0
- webscout/Provider/hermes.py +218 -218
- webscout/Provider/llama3mitril.py +179 -179
- webscout/Provider/llamatutor.py +72 -62
- webscout/Provider/llmchat.py +60 -35
- webscout/Provider/meta.py +794 -794
- webscout/Provider/multichat.py +331 -230
- webscout/Provider/typegpt.py +359 -356
- webscout/Provider/yep.py +5 -5
- webscout/__main__.py +5 -5
- webscout/cli.py +319 -319
- webscout/conversation.py +241 -242
- webscout/exceptions.py +328 -328
- webscout/litagent/__init__.py +28 -28
- webscout/litagent/agent.py +2 -3
- webscout/litprinter/__init__.py +0 -58
- webscout/scout/__init__.py +8 -8
- webscout/scout/core.py +884 -884
- webscout/scout/element.py +459 -459
- webscout/scout/parsers/__init__.py +69 -69
- webscout/scout/parsers/html5lib_parser.py +172 -172
- webscout/scout/parsers/html_parser.py +236 -236
- webscout/scout/parsers/lxml_parser.py +178 -178
- webscout/scout/utils.py +38 -38
- webscout/swiftcli/__init__.py +811 -811
- webscout/update_checker.py +2 -12
- webscout/version.py +1 -1
- webscout/webscout_search.py +1142 -1140
- webscout/webscout_search_async.py +635 -635
- webscout/zeroart/__init__.py +54 -54
- webscout/zeroart/base.py +60 -60
- webscout/zeroart/effects.py +99 -99
- webscout/zeroart/fonts.py +816 -816
- {webscout-7.0.dist-info → webscout-7.2.dist-info}/METADATA +21 -28
- webscout-7.2.dist-info/RECORD +217 -0
- webstoken/__init__.py +30 -30
- webstoken/classifier.py +189 -189
- webstoken/keywords.py +216 -216
- webstoken/language.py +128 -128
- webstoken/ner.py +164 -164
- webstoken/normalizer.py +35 -35
- webstoken/processor.py +77 -77
- webstoken/sentiment.py +206 -206
- webstoken/stemmer.py +73 -73
- webstoken/tagger.py +60 -60
- webstoken/tokenizer.py +158 -158
- webscout/Provider/RUBIKSAI.py +0 -272
- webscout-7.0.dist-info/RECORD +0 -199
- {webscout-7.0.dist-info → webscout-7.2.dist-info}/LICENSE.md +0 -0
- {webscout-7.0.dist-info → webscout-7.2.dist-info}/WHEEL +0 -0
- {webscout-7.0.dist-info → webscout-7.2.dist-info}/entry_points.txt +0 -0
- {webscout-7.0.dist-info → webscout-7.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class LogLevel(Enum):
|
|
4
|
+
DEBUG = 10
|
|
5
|
+
INFO = 20
|
|
6
|
+
WARNING = 30
|
|
7
|
+
ERROR = 40
|
|
8
|
+
CRITICAL = 50
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def get_level(level_str: str) -> 'LogLevel':
|
|
12
|
+
try:
|
|
13
|
+
return LogLevel[level_str.upper()]
|
|
14
|
+
except KeyError:
|
|
15
|
+
raise ValueError(f"Invalid log level: {level_str}")
|
|
16
|
+
|
|
17
|
+
def __lt__(self, other):
|
|
18
|
+
if isinstance(other, LogLevel):
|
|
19
|
+
return self.value < other.value
|
|
20
|
+
return NotImplemented
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from ..styles.colors import LogColors
|
|
8
|
+
from ..styles.formats import LogFormat
|
|
9
|
+
from .level import LogLevel
|
|
10
|
+
|
|
11
|
+
class Logger:
|
|
12
|
+
# Emoji mappings for different log levels
|
|
13
|
+
LEVEL_EMOJIS = {
|
|
14
|
+
LogLevel.DEBUG: "🔍",
|
|
15
|
+
LogLevel.INFO: "ℹ️",
|
|
16
|
+
LogLevel.WARNING: "⚠️",
|
|
17
|
+
LogLevel.ERROR: "❌",
|
|
18
|
+
LogLevel.CRITICAL: "🔥"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
name: str = "LitLogger",
|
|
24
|
+
level: Union[str, LogLevel] = LogLevel.INFO,
|
|
25
|
+
format: str = LogFormat.MODERN,
|
|
26
|
+
handlers: List = None,
|
|
27
|
+
enable_colors: bool = True,
|
|
28
|
+
async_mode: bool = False
|
|
29
|
+
):
|
|
30
|
+
self.name = name
|
|
31
|
+
self.level = LogLevel.get_level(level) if isinstance(level, str) else level
|
|
32
|
+
self.format = format
|
|
33
|
+
self.handlers = handlers or []
|
|
34
|
+
self.enable_colors = enable_colors
|
|
35
|
+
self.async_mode = async_mode
|
|
36
|
+
self._context_data = {}
|
|
37
|
+
self._metrics = {}
|
|
38
|
+
|
|
39
|
+
def _format_message(self, level: LogLevel, message: str, **kwargs) -> str:
|
|
40
|
+
now = datetime.now()
|
|
41
|
+
log_data = {
|
|
42
|
+
"timestamp": now.strftime("%Y-%m-%d %H:%M:%S"),
|
|
43
|
+
"time": now.strftime("%H:%M:%S"), # Add time field
|
|
44
|
+
"date": now.strftime("%Y-%m-%d"), # Add date field
|
|
45
|
+
"level": level.name,
|
|
46
|
+
"name": self.name,
|
|
47
|
+
"message": message,
|
|
48
|
+
"emoji": self.LEVEL_EMOJIS.get(level, ""),
|
|
49
|
+
**self._context_data,
|
|
50
|
+
**kwargs
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if self.format == LogFormat.JSON:
|
|
54
|
+
return json.dumps(log_data)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
formatted = self.format.format(**log_data)
|
|
58
|
+
except KeyError as e:
|
|
59
|
+
# Fallback to a basic format if the specified format fails
|
|
60
|
+
basic_format = "[{time}] {level}: {message}"
|
|
61
|
+
formatted = basic_format.format(**log_data)
|
|
62
|
+
|
|
63
|
+
if self.enable_colors:
|
|
64
|
+
color = LogColors.LEVEL_COLORS.get(level, LogColors.RESET)
|
|
65
|
+
return f"{color}{formatted}{LogColors.RESET}"
|
|
66
|
+
return formatted
|
|
67
|
+
|
|
68
|
+
async def _async_log(self, level: LogLevel, message: str, **kwargs):
|
|
69
|
+
if level.value < self.level.value:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
formatted_message = self._format_message(level, message, **kwargs)
|
|
73
|
+
tasks = []
|
|
74
|
+
for handler in self.handlers:
|
|
75
|
+
if hasattr(handler, 'async_emit'):
|
|
76
|
+
tasks.append(handler.async_emit(formatted_message, level))
|
|
77
|
+
else:
|
|
78
|
+
tasks.append(asyncio.to_thread(handler.emit, formatted_message, level))
|
|
79
|
+
|
|
80
|
+
await asyncio.gather(*tasks)
|
|
81
|
+
|
|
82
|
+
def _sync_log(self, level: LogLevel, message: str, **kwargs):
|
|
83
|
+
if level.value < self.level.value:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
formatted_message = self._format_message(level, message, **kwargs)
|
|
87
|
+
for handler in self.handlers:
|
|
88
|
+
handler.emit(formatted_message, level)
|
|
89
|
+
|
|
90
|
+
def log(self, level: LogLevel, message: str, **kwargs):
|
|
91
|
+
if self.async_mode:
|
|
92
|
+
return asyncio.create_task(self._async_log(level, message, **kwargs))
|
|
93
|
+
return self._sync_log(level, message, **kwargs)
|
|
94
|
+
|
|
95
|
+
def debug(self, message: str, **kwargs):
|
|
96
|
+
self.log(LogLevel.DEBUG, message, **kwargs)
|
|
97
|
+
|
|
98
|
+
def info(self, message: str, **kwargs):
|
|
99
|
+
self.log(LogLevel.INFO, message, **kwargs)
|
|
100
|
+
|
|
101
|
+
def warning(self, message: str, **kwargs):
|
|
102
|
+
self.log(LogLevel.WARNING, message, **kwargs)
|
|
103
|
+
|
|
104
|
+
def error(self, message: str, **kwargs):
|
|
105
|
+
self.log(LogLevel.ERROR, message, **kwargs)
|
|
106
|
+
|
|
107
|
+
def critical(self, message: str, **kwargs):
|
|
108
|
+
self.log(LogLevel.CRITICAL, message, **kwargs)
|
|
109
|
+
|
|
110
|
+
def set_context(self, **kwargs):
|
|
111
|
+
self._context_data.update(kwargs)
|
|
112
|
+
|
|
113
|
+
def clear_context(self):
|
|
114
|
+
self._context_data.clear()
|
|
115
|
+
|
|
116
|
+
def __enter__(self):
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
120
|
+
if exc_type is not None:
|
|
121
|
+
self.error(f"Context exited with error: {exc_val}")
|
|
122
|
+
return False
|
|
123
|
+
return True
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Optional, TextIO
|
|
3
|
+
from ..core.level import LogLevel
|
|
4
|
+
|
|
5
|
+
class ConsoleHandler:
|
|
6
|
+
"""Handler for outputting log messages to the console."""
|
|
7
|
+
|
|
8
|
+
def __init__(self,
|
|
9
|
+
stream: Optional[TextIO] = None,
|
|
10
|
+
level: LogLevel = LogLevel.DEBUG):
|
|
11
|
+
"""
|
|
12
|
+
Initialize console handler.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
stream: Output stream (defaults to sys.stdout)
|
|
16
|
+
level: Minimum log level to output
|
|
17
|
+
"""
|
|
18
|
+
self.stream = stream or sys.stdout
|
|
19
|
+
self.level = level
|
|
20
|
+
|
|
21
|
+
def emit(self, message: str, level: LogLevel):
|
|
22
|
+
"""
|
|
23
|
+
Write log message to console if level is sufficient.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
message: Formatted log message
|
|
27
|
+
level: Message log level
|
|
28
|
+
"""
|
|
29
|
+
if level.value >= self.level.value:
|
|
30
|
+
try:
|
|
31
|
+
self.stream.write(message + "\n")
|
|
32
|
+
self.stream.flush()
|
|
33
|
+
except Exception as e:
|
|
34
|
+
# Fallback to stderr on error
|
|
35
|
+
sys.stderr.write(f"Error in ConsoleHandler: {e}\n")
|
|
36
|
+
sys.stderr.write(message + "\n")
|
|
37
|
+
sys.stderr.flush()
|
|
38
|
+
|
|
39
|
+
async def async_emit(self, message: str, level: LogLevel):
|
|
40
|
+
"""
|
|
41
|
+
Asynchronously write log message to console.
|
|
42
|
+
Just calls emit() since console output is generally fast enough.
|
|
43
|
+
"""
|
|
44
|
+
self.emit(message, level)
|
|
45
|
+
|
|
46
|
+
class ErrorConsoleHandler(ConsoleHandler):
|
|
47
|
+
"""Specialized handler that writes to stderr."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, level: LogLevel = LogLevel.ERROR):
|
|
50
|
+
super().__init__(stream=sys.stderr, level=level)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, 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
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
if level.value < self.level.value:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
log_data = {
|
|
132
|
+
"message": message,
|
|
133
|
+
"level": level.name,
|
|
134
|
+
**self.custom_fields
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if self.batch_size > 0:
|
|
138
|
+
self._batch.append(log_data)
|
|
139
|
+
if len(self._batch) >= self.batch_size:
|
|
140
|
+
await self._send_batch()
|
|
141
|
+
else:
|
|
142
|
+
if self.protocol in ["http", "https"]:
|
|
143
|
+
await self._send_http(log_data)
|
|
144
|
+
else:
|
|
145
|
+
await self._send_tcp(log_data)
|
|
146
|
+
|
|
147
|
+
async def _send_batch(self):
|
|
148
|
+
"""Send batched log messages."""
|
|
149
|
+
if not self._batch:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
batch_data = {"logs": self._batch}
|
|
153
|
+
success = False
|
|
154
|
+
|
|
155
|
+
if self.protocol in ["http", "https"]:
|
|
156
|
+
success = await self._send_http(batch_data)
|
|
157
|
+
else:
|
|
158
|
+
success = await self._send_tcp(batch_data)
|
|
159
|
+
|
|
160
|
+
if success:
|
|
161
|
+
self._batch.clear()
|
|
162
|
+
|
|
163
|
+
async def close(self):
|
|
164
|
+
"""Close network connections."""
|
|
165
|
+
if self._batch:
|
|
166
|
+
await self._send_batch()
|
|
167
|
+
|
|
168
|
+
if self._tcp_socket:
|
|
169
|
+
self._tcp_socket.close()
|
|
170
|
+
self._tcp_socket = None
|
|
171
|
+
|
|
172
|
+
if self._session:
|
|
173
|
+
await self._session.close()
|
|
174
|
+
self._session = None
|