webscout 6.0__py3-none-any.whl → 6.2b0__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 +77 -259
- webscout/Agents/Onlinesearcher.py +22 -10
- webscout/Agents/functioncall.py +2 -2
- webscout/Bard.py +21 -21
- webscout/Extra/autollama.py +37 -20
- webscout/Local/__init__.py +6 -7
- webscout/Local/formats.py +404 -194
- webscout/Local/model.py +1074 -477
- webscout/Local/samplers.py +108 -144
- webscout/Local/thread.py +251 -410
- webscout/Local/ui.py +401 -0
- webscout/Local/utils.py +338 -136
- webscout/Provider/Amigo.py +51 -38
- webscout/Provider/Deepseek.py +7 -6
- webscout/Provider/EDITEE.py +2 -2
- webscout/Provider/GPTWeb.py +1 -1
- webscout/Provider/NinjaChat.py +200 -0
- webscout/Provider/OLLAMA.py +1 -1
- webscout/Provider/Perplexity.py +1 -1
- webscout/Provider/Reka.py +12 -5
- webscout/Provider/TTI/AIuncensored.py +103 -0
- webscout/Provider/TTI/Nexra.py +3 -3
- webscout/Provider/TTI/__init__.py +3 -2
- webscout/Provider/TTI/aiforce.py +2 -2
- webscout/Provider/TTI/imgninza.py +136 -0
- webscout/Provider/TeachAnything.py +0 -3
- webscout/Provider/Youchat.py +1 -1
- webscout/Provider/__init__.py +12 -11
- webscout/Provider/{ChatHub.py → aimathgpt.py} +72 -88
- webscout/Provider/cerebras.py +125 -118
- webscout/Provider/cleeai.py +1 -1
- webscout/Provider/felo_search.py +1 -1
- webscout/Provider/gaurish.py +207 -0
- webscout/Provider/geminiprorealtime.py +160 -0
- webscout/Provider/genspark.py +1 -1
- webscout/Provider/julius.py +8 -3
- webscout/Provider/learnfastai.py +1 -1
- webscout/Provider/promptrefine.py +3 -1
- webscout/Provider/turboseek.py +3 -8
- webscout/Provider/tutorai.py +1 -1
- webscout/__init__.py +2 -43
- webscout/exceptions.py +5 -1
- webscout/tempid.py +4 -73
- webscout/utils.py +3 -0
- webscout/version.py +1 -1
- webscout/webai.py +1 -1
- webscout/webscout_search.py +154 -123
- {webscout-6.0.dist-info → webscout-6.2b0.dist-info}/METADATA +156 -236
- {webscout-6.0.dist-info → webscout-6.2b0.dist-info}/RECORD +53 -54
- webscout/Local/rawdog.py +0 -946
- webscout/Provider/BasedGPT.py +0 -214
- webscout/Provider/TTI/amigo.py +0 -148
- webscout/Provider/aigames.py +0 -213
- webscout/Provider/bixin.py +0 -264
- webscout/Provider/xdash.py +0 -182
- webscout/websx_search.py +0 -19
- {webscout-6.0.dist-info → webscout-6.2b0.dist-info}/LICENSE.md +0 -0
- {webscout-6.0.dist-info → webscout-6.2b0.dist-info}/WHEEL +0 -0
- {webscout-6.0.dist-info → webscout-6.2b0.dist-info}/entry_points.txt +0 -0
- {webscout-6.0.dist-info → webscout-6.2b0.dist-info}/top_level.txt +0 -0
webscout/Local/ui.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import base64
|
|
5
|
+
|
|
6
|
+
from httpx import stream
|
|
7
|
+
|
|
8
|
+
from .model import Model
|
|
9
|
+
|
|
10
|
+
from .utils import (
|
|
11
|
+
InferenceLock,
|
|
12
|
+
download_model,
|
|
13
|
+
print_verbose,
|
|
14
|
+
SPECIAL_STYLE,
|
|
15
|
+
assert_type,
|
|
16
|
+
ERROR_STYLE,
|
|
17
|
+
USER_STYLE,
|
|
18
|
+
BOT_STYLE,
|
|
19
|
+
RESET_ALL
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .thread import Thread
|
|
23
|
+
from cryptography import x509
|
|
24
|
+
from cryptography.x509.oid import NameOID
|
|
25
|
+
from datetime import datetime, timedelta, UTC
|
|
26
|
+
from cryptography.hazmat.primitives import hashes
|
|
27
|
+
from cryptography.hazmat.primitives import serialization
|
|
28
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
29
|
+
from flask import Flask, logging, render_template, request, Response
|
|
30
|
+
from cryptography.hazmat.primitives.serialization import Encoding
|
|
31
|
+
|
|
32
|
+
# Color codes for console output
|
|
33
|
+
YELLOW = SPECIAL_STYLE
|
|
34
|
+
GREEN = USER_STYLE
|
|
35
|
+
RED = ERROR_STYLE
|
|
36
|
+
RESET = RESET_ALL
|
|
37
|
+
BLUE = BOT_STYLE
|
|
38
|
+
|
|
39
|
+
# Warning message for WebUI security
|
|
40
|
+
WARNING = f"""{RED}
|
|
41
|
+
################################################################################
|
|
42
|
+
{RESET}
|
|
43
|
+
|
|
44
|
+
PLEASE KEEP IN MIND
|
|
45
|
+
|
|
46
|
+
The webscout.Local WebUI is not guaranteed to be secure.
|
|
47
|
+
|
|
48
|
+
It is not intended to be exposed to the internet.
|
|
49
|
+
|
|
50
|
+
If you expose the WebUI to the internet, you do so at your own risk.
|
|
51
|
+
|
|
52
|
+
YOU HAVE BEEN WARNED!
|
|
53
|
+
|
|
54
|
+
{RED}
|
|
55
|
+
################################################################################
|
|
56
|
+
{RESET}"""
|
|
57
|
+
|
|
58
|
+
# SSL certificate generation warning
|
|
59
|
+
SSL_CERT_FIRST_TIME_WARNING = (
|
|
60
|
+
f"{YELLOW}You have just generated a new self-signed SSL certificate and "
|
|
61
|
+
f"key. Your browser will probably warn you about an untrusted "
|
|
62
|
+
f"certificate. This is expected, and you may safely proceed to the WebUI. "
|
|
63
|
+
f"Subsequent WebUI sessions will reuse this SSL certificate.{RESET}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Constants
|
|
67
|
+
ASSETS_FOLDER = os.path.join(os.path.dirname(__file__), "assets")
|
|
68
|
+
MAX_LENGTH_INPUT = 100_000 # Maximum input length (characters)
|
|
69
|
+
|
|
70
|
+
def generate_self_signed_ssl_cert() -> None:
|
|
71
|
+
"""Generates a self-signed SSL certificate and key."""
|
|
72
|
+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
73
|
+
public_key = private_key.public_key()
|
|
74
|
+
|
|
75
|
+
# Certificate details
|
|
76
|
+
name = x509.Name([
|
|
77
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, "XY"),
|
|
78
|
+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "DUMMY_STATE"),
|
|
79
|
+
x509.NameAttribute(NameOID.LOCALITY_NAME, "DUMMY_LOCALITY"),
|
|
80
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "EZLLAMA LLC"),
|
|
81
|
+
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
# Certificate builder
|
|
85
|
+
builder = x509.CertificateBuilder(
|
|
86
|
+
subject_name=name,
|
|
87
|
+
issuer_name=name,
|
|
88
|
+
public_key=public_key,
|
|
89
|
+
serial_number=x509.random_serial_number(),
|
|
90
|
+
not_valid_before=datetime.now(tz=UTC),
|
|
91
|
+
not_valid_after=datetime.now(tz=UTC) + timedelta(days=36500),
|
|
92
|
+
)
|
|
93
|
+
builder = builder.add_extension(
|
|
94
|
+
x509.SubjectAlternativeName([x509.DNSName("localhost")]),
|
|
95
|
+
critical=False,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Sign and save the certificate and key
|
|
99
|
+
certificate = builder.sign(private_key=private_key, algorithm=hashes.SHA256())
|
|
100
|
+
with open(f"{ASSETS_FOLDER}/key.pem", "wb") as f:
|
|
101
|
+
f.write(private_key.private_bytes(
|
|
102
|
+
encoding=serialization.Encoding.PEM,
|
|
103
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
104
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
105
|
+
))
|
|
106
|
+
with open(f"{ASSETS_FOLDER}/cert.pem", "wb") as f:
|
|
107
|
+
f.write(certificate.public_bytes(Encoding.PEM))
|
|
108
|
+
|
|
109
|
+
def check_for_ssl_cert() -> bool:
|
|
110
|
+
"""Checks if SSL certificate and key exist."""
|
|
111
|
+
return all(os.path.exists(f"{ASSETS_FOLDER}/{file}.pem") for file in ["cert", "key"])
|
|
112
|
+
|
|
113
|
+
def newline() -> None:
|
|
114
|
+
"""Prints a newline to stderr."""
|
|
115
|
+
print("", end="\n", file=sys.stderr, flush=True)
|
|
116
|
+
|
|
117
|
+
def encode(text: str) -> str:
|
|
118
|
+
"""Encodes a string to base64."""
|
|
119
|
+
return base64.b64encode(text.encode("utf-8")).decode("utf-8")
|
|
120
|
+
|
|
121
|
+
def decode(text: str) -> str:
|
|
122
|
+
"""Decodes a base64 encoded string."""
|
|
123
|
+
return base64.b64decode(text).decode("utf-8")
|
|
124
|
+
|
|
125
|
+
def assert_max_length(text: str) -> None:
|
|
126
|
+
"""Asserts that the length of the text is within the allowed limit."""
|
|
127
|
+
if len(text) > MAX_LENGTH_INPUT:
|
|
128
|
+
raise AssertionError(
|
|
129
|
+
f"Input text exceeds maximum length of {MAX_LENGTH_INPUT} characters."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _print_inference_string(text: str) -> None:
|
|
133
|
+
"""Prints the inference string to stderr."""
|
|
134
|
+
print(
|
|
135
|
+
f"{'#' * 80}\n"
|
|
136
|
+
f"{YELLOW}'''{RESET}{text}{YELLOW}'''{RESET}\n"
|
|
137
|
+
f"{'#' * 80}",
|
|
138
|
+
file=sys.stderr,
|
|
139
|
+
flush=True
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class WebUI:
|
|
144
|
+
"""
|
|
145
|
+
Represents the webscout.Local WebUI server.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, thread: Thread):
|
|
149
|
+
"""
|
|
150
|
+
Initializes the WebUI instance.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
thread (Thread): The Thread instance to use for the WebUI.
|
|
154
|
+
"""
|
|
155
|
+
assert_type(thread, Thread, "thread", "WebUI")
|
|
156
|
+
self.thread = thread
|
|
157
|
+
self.lock = InferenceLock()
|
|
158
|
+
self._cancel_flag = False
|
|
159
|
+
|
|
160
|
+
self.app = Flask(
|
|
161
|
+
__name__,
|
|
162
|
+
static_folder=ASSETS_FOLDER,
|
|
163
|
+
template_folder=ASSETS_FOLDER,
|
|
164
|
+
static_url_path="",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self._log_host = None
|
|
168
|
+
self._log_port = None
|
|
169
|
+
|
|
170
|
+
def log(self, text: str) -> None:
|
|
171
|
+
"""Logs a message to the console."""
|
|
172
|
+
if self._log_host is None or self._log_port is None:
|
|
173
|
+
print_verbose(text)
|
|
174
|
+
else:
|
|
175
|
+
print(
|
|
176
|
+
f"webscout.Local: WebUI @ "
|
|
177
|
+
f"{YELLOW}{self._log_host}{RESET}:"
|
|
178
|
+
f"{YELLOW}{self._log_port}{RESET}: {text}",
|
|
179
|
+
file=sys.stderr,
|
|
180
|
+
flush=True
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _get_context_string(self) -> str:
|
|
184
|
+
"""Returns a string representing the current context usage."""
|
|
185
|
+
thread_len_tokens = self.thread.len_messages()
|
|
186
|
+
max_ctx_len = self.thread.model.context_length
|
|
187
|
+
return f"{thread_len_tokens} / {max_ctx_len} tokens used"
|
|
188
|
+
|
|
189
|
+
def start(self, host: str, port: int = 8080, ssl: bool = False) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Starts the WebUI server.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
host (str): The hostname or IP address to bind the server to.
|
|
195
|
+
port (int, optional): The port to listen on. Defaults to 8080.
|
|
196
|
+
ssl (bool, optional): Whether to use SSL. Defaults to False.
|
|
197
|
+
"""
|
|
198
|
+
print(WARNING, file=sys.stderr, flush=True)
|
|
199
|
+
|
|
200
|
+
assert_type(host, str, "host", "WebUI.start")
|
|
201
|
+
assert_type(port, int, "port", "WebUI.start")
|
|
202
|
+
|
|
203
|
+
self._log_host = host
|
|
204
|
+
self._log_port = port
|
|
205
|
+
|
|
206
|
+
self.log(f"Starting WebUI instance:")
|
|
207
|
+
self.log(f" thread.uuid == {self.thread.uuid}")
|
|
208
|
+
self.log(f" host == {host}")
|
|
209
|
+
self.log(f" port == {port}")
|
|
210
|
+
self.log(f" ssl (HTTPS) == {ssl}")
|
|
211
|
+
|
|
212
|
+
if ssl:
|
|
213
|
+
if not check_for_ssl_cert():
|
|
214
|
+
self.log("Generating self-signed SSL certificate...")
|
|
215
|
+
generate_self_signed_ssl_cert()
|
|
216
|
+
print_verbose(SSL_CERT_FIRST_TIME_WARNING)
|
|
217
|
+
else:
|
|
218
|
+
self.log("Reusing previously generated SSL certificate.")
|
|
219
|
+
|
|
220
|
+
# Flask route definitions
|
|
221
|
+
@self.app.route("/")
|
|
222
|
+
def home():
|
|
223
|
+
return render_template("index.html")
|
|
224
|
+
|
|
225
|
+
@self.app.route("/convo", methods=["GET"])
|
|
226
|
+
def convo():
|
|
227
|
+
msgs_dict = {i: {encode(msg["role"]): encode(msg["content"])}
|
|
228
|
+
for i, msg in enumerate(self.thread.messages)}
|
|
229
|
+
json_convo = json.dumps(msgs_dict)
|
|
230
|
+
return json_convo, 200, {"ContentType": "application/json"}
|
|
231
|
+
|
|
232
|
+
@self.app.route("/cancel", methods=["POST"])
|
|
233
|
+
def cancel():
|
|
234
|
+
newline()
|
|
235
|
+
self.log("Hit cancel endpoint - flag is set.")
|
|
236
|
+
self._cancel_flag = True
|
|
237
|
+
return "", 200
|
|
238
|
+
|
|
239
|
+
@self.app.route("/submit", methods=["POST"])
|
|
240
|
+
def submit():
|
|
241
|
+
self.log("Hit submit endpoint.")
|
|
242
|
+
prompt = decode(request.data)
|
|
243
|
+
assert_max_length(prompt)
|
|
244
|
+
|
|
245
|
+
if not prompt:
|
|
246
|
+
self.log("Empty prompt submitted. Ignoring.")
|
|
247
|
+
return "", 200
|
|
248
|
+
|
|
249
|
+
# Pass the stream variable to the generate function
|
|
250
|
+
def generate(stream=stream):
|
|
251
|
+
with self.lock:
|
|
252
|
+
self.thread.add_message("user", prompt)
|
|
253
|
+
print(f"{GREEN}{prompt}{RESET}", file=sys.stderr)
|
|
254
|
+
inf_str = self.thread.inference_str_from_messages()
|
|
255
|
+
_print_inference_string(inf_str)
|
|
256
|
+
|
|
257
|
+
if stream:
|
|
258
|
+
token_generator = self.thread.model.stream(
|
|
259
|
+
inf_str,
|
|
260
|
+
stops=self.thread.format["stops"],
|
|
261
|
+
sampler=self.thread.sampler
|
|
262
|
+
)
|
|
263
|
+
response = ""
|
|
264
|
+
for token in token_generator:
|
|
265
|
+
if self._cancel_flag:
|
|
266
|
+
print(file=sys.stderr)
|
|
267
|
+
self.log("Canceling generation from /submit.")
|
|
268
|
+
self._cancel_flag = False
|
|
269
|
+
return "", 418 # I'm a teapot
|
|
270
|
+
|
|
271
|
+
tok_text = token['choices'][0]['text']
|
|
272
|
+
response += tok_text
|
|
273
|
+
print(f'{BLUE}{tok_text}{RESET}', end='', flush=True, file=sys.stderr)
|
|
274
|
+
yield encode(tok_text) + "\n"
|
|
275
|
+
else:
|
|
276
|
+
response = self.thread.model.generate(
|
|
277
|
+
inf_str,
|
|
278
|
+
stops=self.thread.format["stops"],
|
|
279
|
+
sampler=self.thread.sampler
|
|
280
|
+
)
|
|
281
|
+
# Simulate streaming by yielding chunks of the content
|
|
282
|
+
chunk_size = self.stream_chunk_size
|
|
283
|
+
for i in range(0, len(response), chunk_size):
|
|
284
|
+
chunk = response[i:i + chunk_size]
|
|
285
|
+
yield encode(chunk) + "\n"
|
|
286
|
+
|
|
287
|
+
self._cancel_flag = False
|
|
288
|
+
newline()
|
|
289
|
+
self.thread.add_message("bot", response)
|
|
290
|
+
|
|
291
|
+
return Response(generate(), mimetype="text/plain")
|
|
292
|
+
|
|
293
|
+
@self.app.route("/reset", methods=["POST"])
|
|
294
|
+
def reset():
|
|
295
|
+
self.thread.reset()
|
|
296
|
+
self.log("Thread reset.")
|
|
297
|
+
return "", 200
|
|
298
|
+
|
|
299
|
+
@self.app.route("/get_context_string", methods=["GET"])
|
|
300
|
+
def get_context_string():
|
|
301
|
+
return json.dumps({"text": encode(self._get_context_string())}), 200, {"ContentType": "application/json"}
|
|
302
|
+
|
|
303
|
+
@self.app.route("/remove", methods=["POST"])
|
|
304
|
+
def remove():
|
|
305
|
+
if len(self.thread.messages) > 1: # Prevent deleting the system message
|
|
306
|
+
self.thread.messages.pop(-1)
|
|
307
|
+
self.log("Removed last message.")
|
|
308
|
+
return "", 200
|
|
309
|
+
else:
|
|
310
|
+
self.log("No previous message to remove.")
|
|
311
|
+
return "", 418 # I'm a teapot
|
|
312
|
+
|
|
313
|
+
@self.app.route("/trigger", methods=["POST"])
|
|
314
|
+
def trigger():
|
|
315
|
+
self.log("Hit trigger endpoint.")
|
|
316
|
+
prompt = decode(request.data)
|
|
317
|
+
assert_max_length(prompt)
|
|
318
|
+
|
|
319
|
+
if prompt:
|
|
320
|
+
self.log(f"Trigger with prompt: {prompt!r}")
|
|
321
|
+
else:
|
|
322
|
+
self.log("Trigger without prompt.")
|
|
323
|
+
|
|
324
|
+
# Pass stream to the generate function
|
|
325
|
+
def generate(stream=stream):
|
|
326
|
+
with self.lock:
|
|
327
|
+
inf_str = self.thread.inference_str_from_messages() + prompt
|
|
328
|
+
_print_inference_string(inf_str)
|
|
329
|
+
|
|
330
|
+
if stream:
|
|
331
|
+
token_generator = self.thread.model.stream(
|
|
332
|
+
inf_str,
|
|
333
|
+
stops=self.thread.format["stops"],
|
|
334
|
+
sampler=self.thread.sampler
|
|
335
|
+
)
|
|
336
|
+
response = ""
|
|
337
|
+
for token in token_generator:
|
|
338
|
+
if self._cancel_flag:
|
|
339
|
+
print(file=sys.stderr)
|
|
340
|
+
self.log("Canceling generation from /trigger.")
|
|
341
|
+
self._cancel_flag = False
|
|
342
|
+
return "", 418 # I'm a teapot
|
|
343
|
+
|
|
344
|
+
tok_text = token["choices"][0]["text"]
|
|
345
|
+
response += tok_text
|
|
346
|
+
print(f"{BLUE}{tok_text}{RESET}", end='', flush=True)
|
|
347
|
+
yield encode(tok_text) + "\n"
|
|
348
|
+
else:
|
|
349
|
+
response = self.thread.model.generate(
|
|
350
|
+
inf_str,
|
|
351
|
+
stops=self.thread.format["stops"],
|
|
352
|
+
sampler=self.thread.sampler
|
|
353
|
+
)
|
|
354
|
+
# Simulate streaming by yielding chunks of the content
|
|
355
|
+
chunk_size = self.stream_chunk_size
|
|
356
|
+
for i in range(0, len(response), chunk_size):
|
|
357
|
+
chunk = response[i:i + chunk_size]
|
|
358
|
+
yield encode(chunk) + "\n"
|
|
359
|
+
|
|
360
|
+
self._cancel_flag = False
|
|
361
|
+
print("", file=sys.stderr)
|
|
362
|
+
self.thread.add_message("bot", prompt + response)
|
|
363
|
+
|
|
364
|
+
return Response(generate(), mimetype="text/plain")
|
|
365
|
+
|
|
366
|
+
@self.app.route("/summarize", methods=["GET"])
|
|
367
|
+
def summarize():
|
|
368
|
+
with self.lock:
|
|
369
|
+
summary = self.thread.summarize()
|
|
370
|
+
self.log(f"Generated summary: {BLUE}{summary!r}{RESET}")
|
|
371
|
+
return encode(summary), 200, {"ContentType": "text/plain"}
|
|
372
|
+
|
|
373
|
+
if not self.thread.model.is_loaded():
|
|
374
|
+
self.log("Loading model...")
|
|
375
|
+
self.thread.model.load()
|
|
376
|
+
else:
|
|
377
|
+
self.log("Model is already loaded.")
|
|
378
|
+
|
|
379
|
+
self.log("Warming up thread...")
|
|
380
|
+
self.thread.warmup()
|
|
381
|
+
|
|
382
|
+
self.log("Running Flask app...")
|
|
383
|
+
try:
|
|
384
|
+
self.app.run(
|
|
385
|
+
host=host,
|
|
386
|
+
port=port,
|
|
387
|
+
ssl_context=(
|
|
388
|
+
(f"{ASSETS_FOLDER}/cert.pem", f"{ASSETS_FOLDER}/key.pem")
|
|
389
|
+
if ssl
|
|
390
|
+
else None
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
except Exception as exc:
|
|
394
|
+
newline()
|
|
395
|
+
self.log(f"{RED}Exception in Flask: {exc}{RESET}")
|
|
396
|
+
raise exc
|
|
397
|
+
else:
|
|
398
|
+
newline()
|
|
399
|
+
self._log_host = None
|
|
400
|
+
self._log_port = None
|
|
401
|
+
|