webscout 7.1__py3-none-any.whl → 7.3__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.

Files changed (154) hide show
  1. webscout/AIauto.py +191 -191
  2. webscout/AIbase.py +122 -122
  3. webscout/AIutel.py +440 -440
  4. webscout/Bard.py +343 -161
  5. webscout/DWEBS.py +489 -492
  6. webscout/Extra/YTToolkit/YTdownloader.py +995 -995
  7. webscout/Extra/YTToolkit/__init__.py +2 -2
  8. webscout/Extra/YTToolkit/transcriber.py +476 -479
  9. webscout/Extra/YTToolkit/ytapi/channel.py +307 -307
  10. webscout/Extra/YTToolkit/ytapi/playlist.py +58 -58
  11. webscout/Extra/YTToolkit/ytapi/pool.py +7 -7
  12. webscout/Extra/YTToolkit/ytapi/utils.py +62 -62
  13. webscout/Extra/YTToolkit/ytapi/video.py +103 -103
  14. webscout/Extra/autocoder/__init__.py +9 -9
  15. webscout/Extra/autocoder/autocoder_utiles.py +199 -199
  16. webscout/Extra/autocoder/rawdog.py +5 -7
  17. webscout/Extra/autollama.py +230 -230
  18. webscout/Extra/gguf.py +3 -3
  19. webscout/Extra/weather.py +171 -171
  20. webscout/LLM.py +442 -442
  21. webscout/Litlogger/__init__.py +67 -681
  22. webscout/Litlogger/core/__init__.py +6 -0
  23. webscout/Litlogger/core/level.py +23 -0
  24. webscout/Litlogger/core/logger.py +166 -0
  25. webscout/Litlogger/handlers/__init__.py +12 -0
  26. webscout/Litlogger/handlers/console.py +33 -0
  27. webscout/Litlogger/handlers/file.py +143 -0
  28. webscout/Litlogger/handlers/network.py +173 -0
  29. webscout/Litlogger/styles/__init__.py +7 -0
  30. webscout/Litlogger/styles/colors.py +249 -0
  31. webscout/Litlogger/styles/formats.py +460 -0
  32. webscout/Litlogger/styles/text.py +87 -0
  33. webscout/Litlogger/utils/__init__.py +6 -0
  34. webscout/Litlogger/utils/detectors.py +154 -0
  35. webscout/Litlogger/utils/formatters.py +200 -0
  36. webscout/Provider/AISEARCH/DeepFind.py +250 -250
  37. webscout/Provider/AISEARCH/ISou.py +277 -0
  38. webscout/Provider/AISEARCH/__init__.py +2 -1
  39. webscout/Provider/Blackboxai.py +3 -3
  40. webscout/Provider/ChatGPTGratis.py +226 -0
  41. webscout/Provider/Cloudflare.py +3 -4
  42. webscout/Provider/DeepSeek.py +218 -0
  43. webscout/Provider/Deepinfra.py +40 -24
  44. webscout/Provider/Free2GPT.py +131 -124
  45. webscout/Provider/Gemini.py +100 -115
  46. webscout/Provider/Glider.py +3 -3
  47. webscout/Provider/Groq.py +5 -1
  48. webscout/Provider/Jadve.py +3 -3
  49. webscout/Provider/Marcus.py +191 -192
  50. webscout/Provider/Netwrck.py +3 -3
  51. webscout/Provider/PI.py +2 -2
  52. webscout/Provider/PizzaGPT.py +2 -3
  53. webscout/Provider/QwenLM.py +311 -0
  54. webscout/Provider/TTI/AiForce/__init__.py +22 -22
  55. webscout/Provider/TTI/AiForce/async_aiforce.py +257 -257
  56. webscout/Provider/TTI/AiForce/sync_aiforce.py +242 -242
  57. webscout/Provider/TTI/FreeAIPlayground/__init__.py +9 -0
  58. webscout/Provider/TTI/FreeAIPlayground/async_freeaiplayground.py +206 -0
  59. webscout/Provider/TTI/FreeAIPlayground/sync_freeaiplayground.py +192 -0
  60. webscout/Provider/TTI/Nexra/__init__.py +22 -22
  61. webscout/Provider/TTI/Nexra/async_nexra.py +286 -286
  62. webscout/Provider/TTI/Nexra/sync_nexra.py +258 -258
  63. webscout/Provider/TTI/PollinationsAI/__init__.py +23 -23
  64. webscout/Provider/TTI/PollinationsAI/async_pollinations.py +330 -330
  65. webscout/Provider/TTI/PollinationsAI/sync_pollinations.py +285 -285
  66. webscout/Provider/TTI/__init__.py +2 -1
  67. webscout/Provider/TTI/artbit/__init__.py +22 -22
  68. webscout/Provider/TTI/artbit/async_artbit.py +184 -184
  69. webscout/Provider/TTI/artbit/sync_artbit.py +176 -176
  70. webscout/Provider/TTI/blackbox/__init__.py +4 -4
  71. webscout/Provider/TTI/blackbox/async_blackbox.py +212 -212
  72. webscout/Provider/TTI/blackbox/sync_blackbox.py +199 -199
  73. webscout/Provider/TTI/deepinfra/__init__.py +4 -4
  74. webscout/Provider/TTI/deepinfra/async_deepinfra.py +227 -227
  75. webscout/Provider/TTI/deepinfra/sync_deepinfra.py +199 -199
  76. webscout/Provider/TTI/huggingface/__init__.py +22 -22
  77. webscout/Provider/TTI/huggingface/async_huggingface.py +199 -199
  78. webscout/Provider/TTI/huggingface/sync_huggingface.py +195 -195
  79. webscout/Provider/TTI/imgninza/__init__.py +4 -4
  80. webscout/Provider/TTI/imgninza/async_ninza.py +214 -214
  81. webscout/Provider/TTI/imgninza/sync_ninza.py +209 -209
  82. webscout/Provider/TTI/talkai/__init__.py +4 -4
  83. webscout/Provider/TTI/talkai/async_talkai.py +229 -229
  84. webscout/Provider/TTI/talkai/sync_talkai.py +207 -207
  85. webscout/Provider/TTS/deepgram.py +182 -182
  86. webscout/Provider/TTS/elevenlabs.py +136 -136
  87. webscout/Provider/TTS/gesserit.py +150 -150
  88. webscout/Provider/TTS/murfai.py +138 -138
  89. webscout/Provider/TTS/parler.py +133 -134
  90. webscout/Provider/TTS/streamElements.py +360 -360
  91. webscout/Provider/TTS/utils.py +280 -280
  92. webscout/Provider/TTS/voicepod.py +116 -116
  93. webscout/Provider/TextPollinationsAI.py +28 -8
  94. webscout/Provider/WiseCat.py +193 -0
  95. webscout/Provider/__init__.py +146 -134
  96. webscout/Provider/cerebras.py +242 -227
  97. webscout/Provider/chatglm.py +204 -204
  98. webscout/Provider/dgaf.py +2 -3
  99. webscout/Provider/freeaichat.py +221 -0
  100. webscout/Provider/gaurish.py +2 -3
  101. webscout/Provider/geminiapi.py +208 -208
  102. webscout/Provider/granite.py +223 -0
  103. webscout/Provider/hermes.py +218 -218
  104. webscout/Provider/llama3mitril.py +179 -179
  105. webscout/Provider/llamatutor.py +3 -3
  106. webscout/Provider/llmchat.py +2 -3
  107. webscout/Provider/meta.py +794 -794
  108. webscout/Provider/multichat.py +331 -331
  109. webscout/Provider/typegpt.py +359 -359
  110. webscout/Provider/yep.py +3 -3
  111. webscout/__init__.py +1 -0
  112. webscout/__main__.py +5 -5
  113. webscout/cli.py +319 -319
  114. webscout/conversation.py +241 -242
  115. webscout/exceptions.py +328 -328
  116. webscout/litagent/__init__.py +28 -28
  117. webscout/litagent/agent.py +2 -3
  118. webscout/litprinter/__init__.py +0 -58
  119. webscout/scout/__init__.py +8 -8
  120. webscout/scout/core.py +884 -884
  121. webscout/scout/element.py +459 -459
  122. webscout/scout/parsers/__init__.py +69 -69
  123. webscout/scout/parsers/html5lib_parser.py +172 -172
  124. webscout/scout/parsers/html_parser.py +236 -236
  125. webscout/scout/parsers/lxml_parser.py +178 -178
  126. webscout/scout/utils.py +38 -38
  127. webscout/swiftcli/__init__.py +811 -811
  128. webscout/update_checker.py +2 -12
  129. webscout/version.py +1 -1
  130. webscout/webscout_search.py +87 -6
  131. webscout/webscout_search_async.py +58 -1
  132. webscout/yep_search.py +297 -0
  133. webscout/zeroart/__init__.py +54 -54
  134. webscout/zeroart/base.py +60 -60
  135. webscout/zeroart/effects.py +99 -99
  136. webscout/zeroart/fonts.py +816 -816
  137. {webscout-7.1.dist-info → webscout-7.3.dist-info}/METADATA +62 -22
  138. webscout-7.3.dist-info/RECORD +223 -0
  139. {webscout-7.1.dist-info → webscout-7.3.dist-info}/WHEEL +1 -1
  140. webstoken/__init__.py +30 -30
  141. webstoken/classifier.py +189 -189
  142. webstoken/keywords.py +216 -216
  143. webstoken/language.py +128 -128
  144. webstoken/ner.py +164 -164
  145. webstoken/normalizer.py +35 -35
  146. webstoken/processor.py +77 -77
  147. webstoken/sentiment.py +206 -206
  148. webstoken/stemmer.py +73 -73
  149. webstoken/tagger.py +60 -60
  150. webstoken/tokenizer.py +158 -158
  151. webscout-7.1.dist-info/RECORD +0 -198
  152. {webscout-7.1.dist-info → webscout-7.3.dist-info}/LICENSE.md +0 -0
  153. {webscout-7.1.dist-info → webscout-7.3.dist-info}/entry_points.txt +0 -0
  154. {webscout-7.1.dist-info → webscout-7.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,6 @@
1
+ """Core logging functionality."""
2
+
3
+ from .logger import Logger
4
+ from .level import LogLevel
5
+
6
+ __all__ = ["Logger", "LogLevel"]
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,166 @@
1
+
2
+ import asyncio
3
+ import sys
4
+ import threading
5
+ import traceback
6
+ from datetime import datetime
7
+ from typing import Any, Dict, List, Optional, Union
8
+
9
+ from ..core.level import LogLevel
10
+ from ..styles.formats import LogFormat
11
+ from ..styles.colors import LogColors
12
+
13
+ class Logger:
14
+ # Emoji mappings for different log levels
15
+ LEVEL_EMOJIS = {
16
+ LogLevel.DEBUG: "🔍",
17
+ LogLevel.INFO: "ℹ️",
18
+ LogLevel.WARNING: "⚠️",
19
+ LogLevel.ERROR: "❌",
20
+ LogLevel.CRITICAL: "🔥"
21
+ }
22
+
23
+ def __init__(
24
+ self,
25
+ name: str = "LitLogger",
26
+ level: Union[str, LogLevel, None] = None,
27
+ format: Union[str, LogFormat] = LogFormat.MODERN_EMOJI,
28
+ handlers: List = None,
29
+ enable_colors: bool = True,
30
+ async_mode: bool = False,
31
+ show_thread: bool = True,
32
+ show_context: bool = True
33
+ ):
34
+ self.name = name
35
+ self.level = LogLevel.NOTSET if level is None else (
36
+ LogLevel.get_level(level) if isinstance(level, str) else level
37
+ )
38
+ self.format = format
39
+ self.enable_colors = enable_colors
40
+ self.async_mode = async_mode
41
+ self.show_thread = show_thread
42
+ self.show_context = show_context
43
+ self._context_data = {}
44
+
45
+ # Initialize with default console handler if none provided
46
+ if handlers is None:
47
+ from ..handlers.console import ConsoleHandler
48
+ self.handlers = [ConsoleHandler(level=self.level)]
49
+ else:
50
+ self.handlers = handlers
51
+
52
+ def _format_message(self, level: LogLevel, message: str, **kwargs) -> str:
53
+ now = datetime.now()
54
+ emoji = self.LEVEL_EMOJIS.get(level, "") if self.enable_colors else ""
55
+
56
+ log_data = {
57
+ "timestamp": now.strftime("%H:%M:%S"),
58
+ "level": level.name,
59
+ "name": self.name,
60
+ "message": str(message),
61
+ "emoji": emoji,
62
+ "thread": threading.current_thread().name if self.show_thread else "",
63
+ }
64
+
65
+ # Add context data
66
+ if self.show_context and self._context_data:
67
+ log_data.update(self._context_data)
68
+
69
+ # Add extra kwargs
70
+ log_data.update(kwargs)
71
+
72
+ # Format exception if present
73
+ if 'exc_info' in kwargs:
74
+ exc_info = kwargs['exc_info']
75
+ if exc_info:
76
+ exception_text = ''.join(traceback.format_exception(*exc_info))
77
+ log_data['message'] = f"{message}\n{exception_text}"
78
+
79
+ try:
80
+ base_message = f"{emoji} [{log_data['timestamp']}] {level.name} {log_data['message']}"
81
+ return base_message
82
+ except Exception as e:
83
+ return f"[{log_data['timestamp']}] {level.name}: {message}"
84
+
85
+ def _log(self, level: LogLevel, message: str, **kwargs):
86
+ if self.async_mode:
87
+ loop = asyncio.get_event_loop()
88
+ if loop.is_running():
89
+ return asyncio.create_task(self._async_log(level, message, **kwargs))
90
+ else:
91
+ return loop.run_until_complete(self._async_log(level, message, **kwargs))
92
+ return self._sync_log(level, message, **kwargs)
93
+
94
+ def debug(self, message: str, **kwargs):
95
+ self._log(LogLevel.DEBUG, message, **kwargs)
96
+
97
+ def info(self, message: str, **kwargs):
98
+ self._log(LogLevel.INFO, message, **kwargs)
99
+
100
+ def warning(self, message: str, **kwargs):
101
+ self._log(LogLevel.WARNING, message, **kwargs)
102
+
103
+ def error(self, message: str, **kwargs):
104
+ self._log(LogLevel.ERROR, message, **kwargs)
105
+
106
+ def critical(self, message: str, **kwargs):
107
+ self._log(LogLevel.CRITICAL, message, **kwargs)
108
+
109
+ def exception(self, message: str, exc_info=True, **kwargs):
110
+ """
111
+ Log an exception with traceback.
112
+
113
+ Args:
114
+ message: The message to log
115
+ exc_info: If True, adds exception info to the message. Can also be a tuple (type, value, traceback)
116
+ **kwargs: Additional key-value pairs to log
117
+ """
118
+ if exc_info:
119
+ if not isinstance(exc_info, tuple):
120
+ exc_info = sys.exc_info()
121
+ kwargs['exc_info'] = exc_info
122
+ self.error(message, **kwargs)
123
+
124
+ def _sync_log(self, level: LogLevel, message: str, **kwargs):
125
+ if self._should_log(level):
126
+ formatted_message = self._format_message(level, message, **kwargs)
127
+ for handler in self.handlers:
128
+ if handler.level == LogLevel.NOTSET or level.value >= handler.level.value:
129
+ handler.emit(formatted_message, level)
130
+
131
+ async def _async_log(self, level: LogLevel, message: str, **kwargs):
132
+ if self._should_log(level):
133
+ formatted_message = self._format_message(level, message, **kwargs)
134
+ tasks = []
135
+ for handler in self.handlers:
136
+ if handler.level == LogLevel.NOTSET or level.value >= handler.level.value:
137
+ if hasattr(handler, 'async_emit'):
138
+ tasks.append(handler.async_emit(formatted_message, level))
139
+ else:
140
+ tasks.append(asyncio.to_thread(handler.emit, formatted_message, level))
141
+ await asyncio.gather(*tasks)
142
+
143
+ def _should_log(self, level: LogLevel) -> bool:
144
+ return self.level == LogLevel.NOTSET or level.value >= self.level.value
145
+
146
+ def set_context(self, **kwargs):
147
+ self._context_data.update(kwargs)
148
+
149
+ def clear_context(self):
150
+ self._context_data.clear()
151
+
152
+ def set_style(self, style: str):
153
+ """Set logger style format."""
154
+ if style in LogFormat.TEMPLATES:
155
+ self.format = LogFormat.TEMPLATES[style]
156
+ else:
157
+ raise ValueError(f"Unknown style: {style}")
158
+
159
+ def __enter__(self):
160
+ return self
161
+
162
+ def __exit__(self, exc_type, exc_val, exc_tb):
163
+ if exc_type is not None:
164
+ self.error(f"Context exited with error: {exc_val}")
165
+ return False
166
+ 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,33 @@
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)
@@ -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,173 @@
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
@@ -0,0 +1,7 @@
1
+ """Styling and formatting utilities."""
2
+
3
+ from .colors import LogColors
4
+ from .formats import LogFormat
5
+ from .text import TextStyle
6
+
7
+ __all__ = ["LogColors", "LogFormat", "TextStyle"]