makcu 0.2.0__py3-none-any.whl → 0.2.1__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.
makcu/__init__.py CHANGED
@@ -1,13 +1,4 @@
1
- """
2
- Makcu Python Library v2.0
3
-
4
- High-performance library for controlling Makcu devices with async support,
5
- zero-delay command execution, and automatic reconnection.
6
- """
7
-
8
1
  from typing import List
9
-
10
- # Import main components
11
2
  from .controller import MakcuController, create_controller, create_async_controller
12
3
  from .enums import MouseButton
13
4
  from .errors import (
@@ -18,22 +9,15 @@ from .errors import (
18
9
  MakcuResponseError
19
10
  )
20
11
 
21
- # Version info
22
12
  __version__: str = "2.0.0"
23
13
  __author__: str = "SleepyTotem"
24
14
  __license__: str = "GPL"
25
15
 
26
- # Public API
27
16
  __all__: List[str] = [
28
- # Main controller
29
17
  "MakcuController",
30
18
  "create_controller",
31
19
  "create_async_controller",
32
-
33
- # Enums
34
20
  "MouseButton",
35
-
36
- # Errors
37
21
  "MakcuError",
38
22
  "MakcuConnectionError",
39
23
  "MakcuCommandError",
@@ -41,10 +25,8 @@ __all__: List[str] = [
41
25
  "MakcuResponseError",
42
26
  ]
43
27
 
44
- # Convenience imports for backward compatibility
45
28
  from .controller import MakcuController as Controller
46
29
 
47
- # Package metadata
48
30
  __doc__ = """
49
31
  Makcu Python Library provides a high-performance interface for controlling
50
32
  Makcu USB devices. Features include:
makcu/__main__.py CHANGED
@@ -28,16 +28,12 @@ def debug_console():
28
28
 
29
29
  command_counter += 1
30
30
 
31
- # Send command and expect response for most commands
32
31
  response = transport.send_command(cmd, expect_response=True)
33
32
 
34
- # Handle the response properly
35
33
  if response and response.strip():
36
- # If response is just the command echoed back, that means success
37
34
  if response.strip() == cmd:
38
35
  print(f"{cmd}")
39
36
  else:
40
- # This is actual response data (like "km.MAKCU" for version)
41
37
  print(f"{response}")
42
38
  else:
43
39
  print("(no response)")
@@ -65,19 +61,16 @@ def test_port(port: str) -> None:
65
61
  print(f"❌ Unexpected error: {e}")
66
62
 
67
63
  def parse_html_results(html_file: Path):
68
- """Parse test results from the pytest HTML report"""
69
64
  if not html_file.exists():
70
65
  raise FileNotFoundError(f"HTML report not found: {html_file}")
71
66
 
72
67
  with open(html_file, 'r', encoding='utf-8') as f:
73
68
  content = f.read()
74
69
 
75
- # Extract the JSON data from the HTML file
76
70
  match = re.search(r'data-jsonblob="([^"]*)"', content)
77
71
  if not match:
78
72
  raise ValueError("Could not find JSON data in HTML report")
79
73
 
80
- # Decode HTML entities in the JSON string
81
74
  json_str = match.group(1)
82
75
  json_str = json_str.replace('"', '"').replace(''', "'").replace('&', '&')
83
76
 
@@ -89,13 +82,10 @@ def parse_html_results(html_file: Path):
89
82
  test_results = []
90
83
  total_ms = 0
91
84
 
92
- # Filter out the connect_to_port test from display
93
85
  skip_tests = {'test_connect_to_port'}
94
86
 
95
87
  for test_id, test_data_list in data.get('tests', {}).items():
96
- test_name = test_id.split('::')[-1] # Get just the test function name
97
-
98
- # Skip connection test from display
88
+ test_name = test_id.split('::')[-1]
99
89
  if test_name in skip_tests:
100
90
  continue
101
91
 
@@ -103,7 +93,6 @@ def parse_html_results(html_file: Path):
103
93
  status = test_data.get('result', 'UNKNOWN')
104
94
  duration_str = test_data.get('duration', '0 ms')
105
95
 
106
- # Parse duration (format: "X ms")
107
96
  duration_match = re.search(r'(\d+)\s*ms', duration_str)
108
97
  duration_ms = int(duration_match.group(1)) if duration_match else 0
109
98
  total_ms += duration_ms
@@ -113,7 +102,6 @@ def parse_html_results(html_file: Path):
113
102
  return test_results, total_ms
114
103
 
115
104
  def run_tests() -> NoReturn:
116
- """Run tests with beautiful console output"""
117
105
  try:
118
106
  from rich.console import Console
119
107
  from rich.table import Table
@@ -127,7 +115,7 @@ def run_tests() -> NoReturn:
127
115
  console = Console()
128
116
 
129
117
  header = Panel.fit(
130
- "[bold cyan]🧪 Makcu Test Suite v2.0[/bold cyan]\n[dim]High-Performance Python Library[/dim]",
118
+ "[bold cyan]Makcu Test Suite v2.0[/bold cyan]\n[dim]High-Performance Python Library[/dim]",
131
119
  border_style="bright_blue"
132
120
  )
133
121
  console.print(Align.center(header))
@@ -150,25 +138,23 @@ def run_tests() -> NoReturn:
150
138
  ) as progress:
151
139
  task = progress.add_task("[cyan]Running tests...", total=100)
152
140
 
153
- # Run pytest with minimal output, generating HTML report
154
141
  result = subprocess.run(
155
142
  [
156
143
  sys.executable, "-m", "pytest",
157
144
  str(test_file),
158
145
  "--rootdir", str(package_dir),
159
- "-q", # Quiet mode
160
- "--tb=no", # No traceback
146
+ "-q",
147
+ "--tb=no",
161
148
  "--html", str(html_file),
162
149
  "--self-contained-html"
163
150
  ],
164
- stdout=subprocess.DEVNULL, # Hide stdout completely
165
- stderr=subprocess.DEVNULL, # Hide stderr completely
151
+ stdout=subprocess.DEVNULL,
152
+ stderr=subprocess.DEVNULL,
166
153
  text=True
167
154
  )
168
155
 
169
156
  progress.update(task, completed=100)
170
157
 
171
- # Parse results from HTML file
172
158
  try:
173
159
  test_results, total_ms = parse_html_results(html_file)
174
160
  except (FileNotFoundError, ValueError) as e:
@@ -178,7 +164,6 @@ def run_tests() -> NoReturn:
178
164
 
179
165
  elapsed_time = time.time() - start_time
180
166
 
181
- # Table rendering
182
167
  table = Table(title="[bold]Test Results[/bold]", show_header=True, header_style="bold magenta")
183
168
  table.add_column("Test", style="cyan", no_wrap=True)
184
169
  table.add_column("Status", justify="center")
@@ -203,12 +188,12 @@ def run_tests() -> NoReturn:
203
188
  status_text = status
204
189
 
205
190
  time_str = f"{duration_ms}ms" if duration_ms else "-"
206
- if duration_ms < 3:
207
- perf = "[green]Excellent[/green]"
208
- elif duration_ms < 5:
209
- perf = "[cyan]🚀 Great[/cyan]"
210
- elif duration_ms < 10:
211
- perf = "[yellow]👍 Good[/yellow]"
191
+ if duration_ms <= 3:
192
+ perf = "[green]Excellent[/green]"
193
+ elif duration_ms <= 5:
194
+ perf = "[cyan]Great[/cyan]"
195
+ elif duration_ms <= 10:
196
+ perf = "[yellow]Good[/yellow]"
212
197
  elif duration_ms > 0:
213
198
  perf = "[red]🐌 Needs work[/red]"
214
199
  else:
@@ -266,7 +251,6 @@ def run_tests() -> NoReturn:
266
251
  "--self-contained-html"
267
252
  ])
268
253
 
269
- # Try to parse HTML results even in fallback mode
270
254
  try:
271
255
  test_results, total_ms = parse_html_results(html_file)
272
256
  passed = sum(1 for _, status, _ in test_results if status.upper() == "PASSED")
makcu/conftest.py CHANGED
@@ -4,7 +4,6 @@ from makcu import MakcuController, MouseButton
4
4
 
5
5
  @pytest.fixture(scope="session")
6
6
  def makcu(request):
7
- """Session-scoped fixture with final cleanup at end of all tests"""
8
7
  ctrl = MakcuController(fallback_com_port="COM1", debug=False)
9
8
 
10
9
  def cleanup():
makcu/connection.py CHANGED
@@ -17,31 +17,28 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  @dataclass
19
19
  class PendingCommand:
20
- """Tracks a command waiting for response"""
21
20
  command_id: int
22
21
  command: str
23
22
  future: Future
24
23
  timestamp: float
25
24
  expect_response: bool = True
26
- timeout: float = 0.1 # Reduced from 1.0
25
+ timeout: float = 0.1
27
26
 
28
27
  @dataclass
29
28
  class ParsedResponse:
30
- """Parsed response from device"""
31
29
  command_id: Optional[int]
32
30
  content: str
33
31
  is_button_data: bool = False
34
32
  button_mask: Optional[int] = None
35
33
 
36
34
  class SerialTransport:
37
- """Ultra-optimized serial transport for gaming performance"""
38
35
 
39
36
  BAUD_CHANGE_COMMAND = bytearray([0xDE, 0xAD, 0x05, 0x00, 0xA5, 0x00, 0x09, 0x3D, 0x00])
40
- DEFAULT_TIMEOUT = 0.1 # Reduced from 1.0 for gaming
41
- MAX_RECONNECT_ATTEMPTS = 3 # Reduced from 5
42
- RECONNECT_DELAY = 0.1 # Reduced from 0.5
37
+ DEFAULT_TIMEOUT = 0.1
38
+ MAX_RECONNECT_ATTEMPTS = 3
39
+ RECONNECT_DELAY = 0.1
43
40
 
44
- # Pre-computed button maps for faster lookups
41
+
45
42
  BUTTON_MAP = (
46
43
  'left', 'right', 'middle', 'mouse4', 'mouse5'
47
44
  )
@@ -57,14 +54,14 @@ class SerialTransport:
57
54
  def __init__(self, fallback: str = "", debug: bool = False,
58
55
  send_init: bool = True, auto_reconnect: bool = True,
59
56
  override_port: bool = False) -> None:
60
- # Basic config
57
+
61
58
  self._fallback_com_port = fallback
62
59
  self.debug = debug
63
60
  self.send_init = send_init
64
61
  self.auto_reconnect = auto_reconnect
65
62
  self.override_port = override_port
66
63
 
67
- # Connection state
64
+
68
65
  self._is_connected = False
69
66
  self._reconnect_attempts = 0
70
67
  self.port: Optional[str] = None
@@ -72,54 +69,51 @@ class SerialTransport:
72
69
  self.serial: Optional[serial.Serial] = None
73
70
  self._current_baud: Optional[int] = None
74
71
 
75
- # Command tracking with pre-allocated buffer
72
+
76
73
  self._command_counter = 0
77
74
  self._pending_commands: Dict[int, PendingCommand] = {}
78
75
  self._command_lock = threading.Lock()
79
76
 
80
- # Response parsing with optimized buffer
81
- self._parse_buffer = bytearray(1024) # Pre-allocate
77
+
78
+ self._parse_buffer = bytearray(1024)
82
79
  self._buffer_pos = 0
83
- self._response_queue = deque(maxlen=100) # Limit queue size
80
+ self._response_queue = deque(maxlen=100)
84
81
 
85
- # Button state with bitwise operations
82
+
86
83
  self._button_callback: Optional[Callable[[MouseButton, bool], None]] = None
87
84
  self._last_button_mask = 0
88
- self._button_states = 0 # Use single int instead of dict
85
+ self._button_states = 0
89
86
 
90
- # Threading
87
+
91
88
  self._stop_event = threading.Event()
92
89
  self._listener_thread: Optional[threading.Thread] = None
93
90
 
94
- # Logging optimization
91
+
95
92
  self._log_messages: deque = deque(maxlen=100)
96
93
 
97
- # Cache for frequently used data
98
- self._ascii_decode_table = bytes(range(128)) # ASCII lookup table
94
+
95
+ self._ascii_decode_table = bytes(range(128))
99
96
 
100
97
  def _log(self, message: str, level: str = "INFO") -> None:
101
- """Optimized logging - only format when needed"""
102
98
  if not self.debug and level == "DEBUG":
103
99
  return
104
100
 
105
101
  if self.debug:
106
- # Use faster time formatting
102
+
107
103
  timestamp = f"{time.time():.3f}"
108
104
  entry = f"[{timestamp}] [{level}] {message}"
109
105
  self._log_messages.append(entry)
110
106
  print(entry, flush=True)
111
107
 
112
108
  def _generate_command_id(self) -> int:
113
- """Generate unique command ID - optimized with no lock for single-threaded access"""
114
- self._command_counter = (self._command_counter + 1) & 0x2710 # Faster than % 10000
109
+ self._command_counter = (self._command_counter + 1) & 0x2710
115
110
  return self._command_counter
116
111
 
117
112
  def find_com_port(self) -> Optional[str]:
118
- """Optimized port finding with caching"""
119
113
  if self.override_port:
120
114
  return self._fallback_com_port
121
115
 
122
- # Cache the VID:PID string
116
+
123
117
  target_hwid = "VID:PID=1A86:55D3"
124
118
 
125
119
  for port in list_ports.comports():
@@ -134,8 +128,7 @@ class SerialTransport:
134
128
  return None
135
129
 
136
130
  def _parse_response_line(self, line: bytes) -> ParsedResponse:
137
- """Optimized parsing - work with bytes directly"""
138
- # Skip decode for simple checks
131
+
139
132
  if line.startswith(b'>>> '):
140
133
  content = line[4:].decode('ascii', 'ignore').strip()
141
134
  return ParsedResponse(None, content, False)
@@ -144,18 +137,17 @@ class SerialTransport:
144
137
  return ParsedResponse(None, content, False)
145
138
 
146
139
  def _handle_button_data(self, byte_val: int) -> None:
147
- """Optimized button handling with bitwise operations"""
148
140
  if byte_val == self._last_button_mask:
149
141
  return
150
142
 
151
143
  changed_bits = byte_val ^ self._last_button_mask
152
144
 
153
- # Use bitwise operations instead of dict lookups
154
- for bit in range(5): # Only check 5 buttons
145
+
146
+ for bit in range(5):
155
147
  if changed_bits & (1 << bit):
156
148
  is_pressed = bool(byte_val & (1 << bit))
157
149
 
158
- # Update button state in single int
150
+
159
151
  if is_pressed:
160
152
  self._button_states |= (1 << bit)
161
153
  else:
@@ -165,12 +157,11 @@ class SerialTransport:
165
157
  try:
166
158
  self._button_callback(self.BUTTON_ENUM_MAP[bit], is_pressed)
167
159
  except Exception:
168
- pass # Silently ignore callback errors for speed
160
+ pass
169
161
 
170
162
  self._last_button_mask = byte_val
171
163
 
172
164
  def _process_pending_commands(self, content: str) -> None:
173
- """Optimized command processing"""
174
165
  if not content or not self._pending_commands:
175
166
  return
176
167
 
@@ -178,14 +169,14 @@ class SerialTransport:
178
169
  if not self._pending_commands:
179
170
  return
180
171
 
181
- # Get oldest command without min() call
172
+
182
173
  oldest_id = next(iter(self._pending_commands))
183
174
  pending = self._pending_commands[oldest_id]
184
175
 
185
176
  if pending.future.done():
186
177
  return
187
178
 
188
- # Fast string comparison
179
+
189
180
  if content == pending.command:
190
181
  if not pending.expect_response:
191
182
  pending.future.set_result(pending.command)
@@ -195,20 +186,19 @@ class SerialTransport:
195
186
  del self._pending_commands[oldest_id]
196
187
 
197
188
  def _cleanup_timed_out_commands(self) -> None:
198
- """Optimized cleanup - batch operations"""
199
189
  if not self._pending_commands:
200
190
  return
201
191
 
202
192
  current_time = time.time()
203
193
  with self._command_lock:
204
- # Collect timed out commands
194
+
205
195
  timed_out = [
206
196
  (cmd_id, pending)
207
197
  for cmd_id, pending in self._pending_commands.items()
208
198
  if current_time - pending.timestamp > pending.timeout
209
199
  ]
210
200
 
211
- # Batch remove
201
+
212
202
  for cmd_id, pending in timed_out:
213
203
  del self._pending_commands[cmd_id]
214
204
  if not pending.future.done():
@@ -216,44 +206,44 @@ class SerialTransport:
216
206
  MakcuTimeoutError(f"Command #{cmd_id} timed out")
217
207
  )
218
208
 
209
+
219
210
  def _listen(self) -> None:
220
- """Ultra-optimized listener for gaming performance"""
221
- # Pre-allocate buffers
211
+
222
212
  read_buffer = bytearray(4096)
223
213
  line_buffer = bytearray(256)
224
214
  line_pos = 0
225
215
 
226
- # Cache frequently accessed attributes
216
+
227
217
  serial_read = self.serial.read
228
218
  serial_in_waiting = lambda: self.serial.in_waiting
229
219
  is_connected = lambda: self._is_connected
230
220
  stop_requested = self._stop_event.is_set
231
221
 
232
- # Timing for cleanup
222
+
233
223
  last_cleanup = time.time()
234
- cleanup_interval = 0.05 # 50ms cleanup interval
224
+ cleanup_interval = 0.05
235
225
 
236
226
  while is_connected() and not stop_requested():
237
227
  try:
238
- # Check bytes available
228
+
239
229
  bytes_available = serial_in_waiting()
240
230
  if not bytes_available:
241
- time.sleep(0.001) # 1ms sleep to prevent CPU spinning
231
+ time.sleep(0.001)
242
232
  continue
243
233
 
244
- # Read into pre-allocated buffer
234
+
245
235
  bytes_read = serial_read(min(bytes_available, 4096))
246
236
 
247
- # Process bytes
237
+
248
238
  for byte_val in bytes_read:
249
- # Fast button data check
239
+
250
240
  if byte_val < 32 and byte_val not in (0x0D, 0x0A):
251
241
  self._handle_button_data(byte_val)
252
242
  else:
253
- # Build line
254
- if byte_val == 0x0A: # LF
243
+
244
+ if byte_val == 0x0A:
255
245
  if line_pos > 0:
256
- # Process line without allocation
246
+
257
247
  line = bytes(line_buffer[:line_pos])
258
248
  line_pos = 0
259
249
 
@@ -261,12 +251,12 @@ class SerialTransport:
261
251
  response = self._parse_response_line(line)
262
252
  if response.content:
263
253
  self._process_pending_commands(response.content)
264
- elif byte_val != 0x0D: # Ignore CR
265
- if line_pos < 256: # Prevent overflow
254
+ elif byte_val != 0x0D:
255
+ if line_pos < 256:
266
256
  line_buffer[line_pos] = byte_val
267
257
  line_pos += 1
268
258
 
269
- # Periodic cleanup with reduced frequency
259
+
270
260
  current_time = time.time()
271
261
  if current_time - last_cleanup > cleanup_interval:
272
262
  self._cleanup_timed_out_commands()
@@ -278,10 +268,9 @@ class SerialTransport:
278
268
  else:
279
269
  break
280
270
  except Exception:
281
- pass # Silently continue for maximum performance
271
+ pass
282
272
 
283
273
  def _attempt_reconnect(self) -> None:
284
- """Fast reconnection attempt"""
285
274
  if self._reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
286
275
  self._is_connected = False
287
276
  return
@@ -298,12 +287,12 @@ class SerialTransport:
298
287
  if not self.port:
299
288
  raise MakcuConnectionError("Device not found")
300
289
 
301
- # Use write_timeout for faster failure detection
290
+
302
291
  self.serial = serial.Serial(
303
292
  self.port,
304
293
  self.baudrate,
305
- timeout=0.001, # 1ms read timeout
306
- write_timeout=0.01 # 10ms write timeout
294
+ timeout=0.001,
295
+ write_timeout=0.01
307
296
  )
308
297
  self._change_baud_to_4M()
309
298
 
@@ -317,18 +306,16 @@ class SerialTransport:
317
306
  time.sleep(self.RECONNECT_DELAY)
318
307
 
319
308
  def _change_baud_to_4M(self) -> bool:
320
- """Optimized baud rate change"""
321
309
  if self.serial and self.serial.is_open:
322
310
  self.serial.write(self.BAUD_CHANGE_COMMAND)
323
311
  self.serial.flush()
324
- time.sleep(0.02) # Reduced from 0.05
312
+ time.sleep(0.02)
325
313
  self.serial.baudrate = 4000000
326
314
  self._current_baud = 4000000
327
315
  return True
328
316
  return False
329
317
 
330
318
  def connect(self) -> None:
331
- """Optimized connection with minimal overhead"""
332
319
  if self._is_connected:
333
320
  return
334
321
 
@@ -341,12 +328,12 @@ class SerialTransport:
341
328
  raise MakcuConnectionError("Makcu device not found")
342
329
 
343
330
  try:
344
- # Optimized serial settings for gaming
331
+
345
332
  self.serial = serial.Serial(
346
333
  self.port,
347
334
  115200,
348
- timeout=0.001, # 1ms timeout
349
- write_timeout=0.01, # 10ms write timeout
335
+ timeout=0.001,
336
+ write_timeout=0.01,
350
337
  xonxoff=False,
351
338
  rtscts=False,
352
339
  dsrdtr=False
@@ -362,7 +349,7 @@ class SerialTransport:
362
349
  self.serial.write(b"km.buttons(1)\r")
363
350
  self.serial.flush()
364
351
 
365
- # Start high-priority listener thread
352
+
366
353
  self._stop_event.clear()
367
354
  self._listener_thread = threading.Thread(
368
355
  target=self._listen,
@@ -377,13 +364,12 @@ class SerialTransport:
377
364
  raise MakcuConnectionError(f"Failed to connect: {e}")
378
365
 
379
366
  def disconnect(self) -> None:
380
- """Fast disconnect"""
381
367
  self._is_connected = False
382
368
 
383
369
  if self.send_init:
384
370
  self._stop_event.set()
385
371
  if self._listener_thread and self._listener_thread.is_alive():
386
- self._listener_thread.join(timeout=0.1) # Reduced timeout
372
+ self._listener_thread.join(timeout=0.1)
387
373
 
388
374
  with self._command_lock:
389
375
  for pending in self._pending_commands.values():
@@ -397,17 +383,14 @@ class SerialTransport:
397
383
 
398
384
  def send_command(self, command: str, expect_response: bool = False,
399
385
  timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
400
- """Optimized command sending for minimal latency"""
401
386
  if not self._is_connected or not self.serial or not self.serial.is_open:
402
387
  raise MakcuConnectionError("Not connected")
403
388
 
404
- # For commands without response, send and return immediately
405
389
  if not expect_response:
406
390
  self.serial.write(f"{command}\r\n".encode('ascii'))
407
391
  self.serial.flush()
408
392
  return command
409
393
 
410
- # Commands with response need tracking
411
394
  cmd_id = self._generate_command_id()
412
395
  tagged_command = f"{command}#{cmd_id}"
413
396
 
@@ -439,36 +422,29 @@ class SerialTransport:
439
422
 
440
423
  async def async_send_command(self, command: str, expect_response: bool = False,
441
424
  timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
442
- """Async command optimized for gaming"""
443
425
  loop = asyncio.get_running_loop()
444
426
  return await loop.run_in_executor(
445
427
  None, self.send_command, command, expect_response, timeout
446
428
  )
447
429
 
448
430
  def is_connected(self) -> bool:
449
- """Fast connection check"""
450
431
  return self._is_connected and self.serial is not None and self.serial.is_open
451
432
 
452
433
  def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
453
- """Set button callback"""
454
434
  self._button_callback = callback
455
435
 
456
436
  def get_button_states(self) -> Dict[str, bool]:
457
- """Get button states with optimized lookup"""
458
437
  return {
459
438
  self.BUTTON_MAP[i]: bool(self._button_states & (1 << i))
460
439
  for i in range(5)
461
440
  }
462
441
 
463
442
  def get_button_mask(self) -> int:
464
- """Direct mask access"""
465
443
  return self._last_button_mask
466
444
 
467
445
  def enable_button_monitoring(self, enable: bool = True) -> None:
468
- """Fast button monitoring toggle"""
469
446
  self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
470
447
 
471
- # Context managers unchanged but included for completeness
472
448
  async def __aenter__(self):
473
449
  loop = asyncio.get_running_loop()
474
450
  await loop.run_in_executor(None, self.connect)