makcu 0.1.4__py3-none-any.whl → 0.2.0__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,16 +1,78 @@
1
- from .controller import MakcuController
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
+ from typing import List
9
+
10
+ # Import main components
11
+ from .controller import MakcuController, create_controller, create_async_controller
2
12
  from .enums import MouseButton
3
- from .errors import MakcuError, MakcuConnectionError
13
+ from .errors import (
14
+ MakcuError,
15
+ MakcuConnectionError,
16
+ MakcuCommandError,
17
+ MakcuTimeoutError,
18
+ MakcuResponseError
19
+ )
4
20
 
5
- def create_controller(fallback_com_port="", debug=False, send_init=True):
6
- makcu = MakcuController(fallback_com_port, debug=debug, send_init=send_init)
7
- makcu.connect()
8
- return makcu
21
+ # Version info
22
+ __version__: str = "2.0.0"
23
+ __author__: str = "SleepyTotem"
24
+ __license__: str = "GPL"
9
25
 
10
- __all__ = [
26
+ # Public API
27
+ __all__: List[str] = [
28
+ # Main controller
11
29
  "MakcuController",
30
+ "create_controller",
31
+ "create_async_controller",
32
+
33
+ # Enums
12
34
  "MouseButton",
35
+
36
+ # Errors
13
37
  "MakcuError",
14
38
  "MakcuConnectionError",
15
- "create_controller",
16
- ]
39
+ "MakcuCommandError",
40
+ "MakcuTimeoutError",
41
+ "MakcuResponseError",
42
+ ]
43
+
44
+ # Convenience imports for backward compatibility
45
+ from .controller import MakcuController as Controller
46
+
47
+ # Package metadata
48
+ __doc__ = """
49
+ Makcu Python Library provides a high-performance interface for controlling
50
+ Makcu USB devices. Features include:
51
+
52
+ - Full async/await support for modern Python applications
53
+ - Zero-delay command execution with intelligent tracking
54
+ - Automatic device reconnection on disconnect
55
+ - Human-like mouse movement and clicking patterns
56
+ - Comprehensive button and axis locking
57
+ - Real-time button event monitoring
58
+
59
+ Quick Start:
60
+ >>> from makcu import create_controller, MouseButton
61
+ >>> makcu = create_controller()
62
+ >>> makcu.click(MouseButton.LEFT)
63
+ >>> makcu.move(100, 50)
64
+ >>> makcu.disconnect()
65
+
66
+ Async Usage:
67
+ >>> import asyncio
68
+ >>> from makcu import create_async_controller, MouseButton
69
+ >>>
70
+ >>> async def main():
71
+ ... async with await create_async_controller() as makcu:
72
+ ... await makcu.click(MouseButton.LEFT)
73
+ ... await makcu.move(100, 50)
74
+ >>>
75
+ >>> asyncio.run(main())
76
+
77
+ For more information, visit: https://github.com/SleepyTotem/makcu-py-lib
78
+ """
makcu/__main__.py CHANGED
@@ -1,9 +1,12 @@
1
1
  import sys
2
- import webbrowser
3
2
  import os
4
3
  from pathlib import Path
4
+ from typing import List, NoReturn
5
5
  import pytest
6
- from makcu import create_controller, MakcuConnectionError
6
+ import time
7
+ from makcu import create_controller, MakcuConnectionError, MakcuController
8
+ import json
9
+ import re
7
10
 
8
11
  def debug_console():
9
12
  controller = create_controller()
@@ -13,6 +16,8 @@ def debug_console():
13
16
  print("Type a raw command (e.g., km.version()) and press Enter.")
14
17
  print("Type 'exit' or 'quit' to leave.")
15
18
 
19
+ command_counter = 0
20
+
16
21
  while True:
17
22
  try:
18
23
  cmd = input(">>> ").strip()
@@ -21,8 +26,21 @@ def debug_console():
21
26
  if not cmd:
22
27
  continue
23
28
 
29
+ command_counter += 1
30
+
31
+ # Send command and expect response for most commands
24
32
  response = transport.send_command(cmd, expect_response=True)
25
- print(f"{response or '(no response)'}")
33
+
34
+ # Handle the response properly
35
+ if response and response.strip():
36
+ # If response is just the command echoed back, that means success
37
+ if response.strip() == cmd:
38
+ print(f"{cmd}")
39
+ else:
40
+ # This is actual response data (like "km.MAKCU" for version)
41
+ print(f"{response}")
42
+ else:
43
+ print("(no response)")
26
44
 
27
45
  except Exception as e:
28
46
  print(f"⚠️ Error: {e}")
@@ -30,48 +48,247 @@ def debug_console():
30
48
  controller.disconnect()
31
49
  print("Disconnected.")
32
50
 
33
- def test_port(port):
51
+ def test_port(port: str) -> None:
34
52
  try:
35
- print(f"Trying to connect to {port} (without init command)...")
36
- controller = create_controller(fallback_com_port=port, send_init=False)
37
- print(f"✅ Successfully connected to {port}")
38
- controller.disconnect()
53
+ print(f"Trying to connect to {port}...")
54
+ makcu = MakcuController(fallback_com_port=port, send_init=False, override_port=True)
55
+ makcu.connect()
56
+ if makcu.is_connected:
57
+ print(f"✅ Successfully connected to {port}.")
58
+ makcu.disconnect()
39
59
  except MakcuConnectionError as e:
40
- print(f" Failed to connect to {port}: {e}")
60
+ if "FileNotFoundError" in str(e):
61
+ print(f"❌ Port {port} does not exist. Please check the port name.")
62
+ else:
63
+ print(f"❌ Failed to connect to {port}: ")
41
64
  except Exception as e:
42
65
  print(f"❌ Unexpected error: {e}")
43
66
 
44
- def run_tests():
45
- print("🧪 Running Pytest Suite...")
46
-
47
- package_dir = Path(__file__).resolve().parent
48
- test_file = package_dir / "test_suite.py"
49
-
50
- result = pytest.main([
51
- str(test_file),
52
- "--rootdir", str(package_dir),
53
- "-v", "--tb=short",
54
- "--capture=tee-sys",
55
- "--html=latest_pytest.html",
56
- "--self-contained-html"
57
- ])
58
-
59
- report_path = os.path.abspath("latest_pytest.html")
60
- if os.path.exists(report_path):
61
- print(f"📄 Opening test report: {report_path}")
62
- webbrowser.open(f"file://{report_path}")
63
- else:
64
- print(" Report not found. Something went wrong.")
67
+ def parse_html_results(html_file: Path):
68
+ """Parse test results from the pytest HTML report"""
69
+ if not html_file.exists():
70
+ raise FileNotFoundError(f"HTML report not found: {html_file}")
71
+
72
+ with open(html_file, 'r', encoding='utf-8') as f:
73
+ content = f.read()
74
+
75
+ # Extract the JSON data from the HTML file
76
+ match = re.search(r'data-jsonblob="([^"]*)"', content)
77
+ if not match:
78
+ raise ValueError("Could not find JSON data in HTML report")
79
+
80
+ # Decode HTML entities in the JSON string
81
+ json_str = match.group(1)
82
+ json_str = json_str.replace('"', '"').replace(''', "'").replace('&', '&')
83
+
84
+ try:
85
+ data = json.loads(json_str)
86
+ except json.JSONDecodeError as e:
87
+ raise ValueError(f"Failed to parse JSON data: {e}")
88
+
89
+ test_results = []
90
+ total_ms = 0
91
+
92
+ # Filter out the connect_to_port test from display
93
+ skip_tests = {'test_connect_to_port'}
94
+
95
+ 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
99
+ if test_name in skip_tests:
100
+ continue
101
+
102
+ for test_data in test_data_list:
103
+ status = test_data.get('result', 'UNKNOWN')
104
+ duration_str = test_data.get('duration', '0 ms')
105
+
106
+ # Parse duration (format: "X ms")
107
+ duration_match = re.search(r'(\d+)\s*ms', duration_str)
108
+ duration_ms = int(duration_match.group(1)) if duration_match else 0
109
+ total_ms += duration_ms
110
+
111
+ test_results.append((test_name, status, duration_ms))
112
+
113
+ return test_results, total_ms
65
114
 
66
- if result != 0:
67
- print(" Some tests failed.")
68
- else:
69
- print("✅ All tests passed.")
115
+ def run_tests() -> NoReturn:
116
+ """Run tests with beautiful console output"""
117
+ try:
118
+ from rich.console import Console
119
+ from rich.table import Table
120
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
121
+ from rich.panel import Panel
122
+ from rich.align import Align
123
+ from rich import print as rprint
124
+ from rich.text import Text
125
+ import subprocess
126
+
127
+ console = Console()
128
+
129
+ header = Panel.fit(
130
+ "[bold cyan]🧪 Makcu Test Suite v2.0[/bold cyan]\n[dim]High-Performance Python Library[/dim]",
131
+ border_style="bright_blue"
132
+ )
133
+ console.print(Align.center(header))
134
+ console.print()
135
+
136
+ package_dir: Path = Path(__file__).resolve().parent
137
+ test_file: Path = package_dir / "test_suite.py"
138
+ html_file: Path = package_dir.parent / "latest_pytest.html"
139
+
140
+ start_time = time.time()
141
+
142
+ with Progress(
143
+ SpinnerColumn(),
144
+ TextColumn("[progress.description]{task.description}"),
145
+ BarColumn(),
146
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
147
+ TimeElapsedColumn(),
148
+ console=console,
149
+ transient=True
150
+ ) as progress:
151
+ task = progress.add_task("[cyan]Running tests...", total=100)
152
+
153
+ # Run pytest with minimal output, generating HTML report
154
+ result = subprocess.run(
155
+ [
156
+ sys.executable, "-m", "pytest",
157
+ str(test_file),
158
+ "--rootdir", str(package_dir),
159
+ "-q", # Quiet mode
160
+ "--tb=no", # No traceback
161
+ "--html", str(html_file),
162
+ "--self-contained-html"
163
+ ],
164
+ stdout=subprocess.DEVNULL, # Hide stdout completely
165
+ stderr=subprocess.DEVNULL, # Hide stderr completely
166
+ text=True
167
+ )
168
+
169
+ progress.update(task, completed=100)
170
+
171
+ # Parse results from HTML file
172
+ try:
173
+ test_results, total_ms = parse_html_results(html_file)
174
+ except (FileNotFoundError, ValueError) as e:
175
+ console.print(f"[red]❌ Failed to parse test results: {e}[/red]")
176
+ console.print(f"[yellow]⚠️ pytest exit code: {result.returncode}[/yellow]")
177
+ sys.exit(1)
178
+
179
+ elapsed_time = time.time() - start_time
180
+
181
+ # Table rendering
182
+ table = Table(title="[bold]Test Results[/bold]", show_header=True, header_style="bold magenta")
183
+ table.add_column("Test", style="cyan", no_wrap=True)
184
+ table.add_column("Status", justify="center")
185
+ table.add_column("Time", justify="right", style="yellow")
186
+ table.add_column("Performance", justify="center")
187
+
188
+ passed = failed = skipped = 0
189
+
190
+ for test_name, status, duration_ms in test_results:
191
+ display_name = test_name.replace("test_", "").replace("_", " ").title()
192
+
193
+ if status.upper() == "PASSED":
194
+ status_text = "[green]✅ PASSED[/green]"
195
+ passed += 1
196
+ elif status.upper() == "FAILED":
197
+ status_text = "[red]❌ FAILED[/red]"
198
+ failed += 1
199
+ elif status.upper() == "SKIPPED":
200
+ status_text = "[yellow]⏭️ SKIPPED[/yellow]"
201
+ skipped += 1
202
+ else:
203
+ status_text = status
204
+
205
+ 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]"
212
+ elif duration_ms > 0:
213
+ perf = "[red]🐌 Needs work[/red]"
214
+ else:
215
+ perf = "-"
216
+
217
+ table.add_row(display_name, status_text, time_str, perf)
218
+
219
+ console.print("\n")
220
+ console.print(table)
221
+ console.print()
222
+
223
+ summary = Table.grid(padding=1)
224
+ summary.add_column(style="bold cyan", justify="right")
225
+ summary.add_column(justify="left")
226
+ summary.add_row("Total Tests:", str(len(test_results)))
227
+ summary.add_row("Passed:", f"[green]{passed}[/green]")
228
+ summary.add_row("Failed:", f"[red]{failed}[/red]" if failed else str(failed))
229
+ summary.add_row("Skipped:", f"[yellow]{skipped}[/yellow]" if skipped else str(skipped))
230
+ summary.add_row("Total Time:", f"{elapsed_time:.2f}s")
231
+ summary.add_row("Avg Time/Test:", f"{total_ms/len(test_results):.1f}ms" if test_results else "0ms")
232
+
233
+ console.print(Align.center(Panel(summary, title="[bold]Summary[/bold]", border_style="blue", expand=False)))
234
+ console.print()
235
+
236
+ if test_results:
237
+ avg_time = total_ms / len(test_results)
238
+ if avg_time < 3:
239
+ perf_text = Text("Performance: ELITE - Ready for 360Hz+ gaming!", style="bold bright_green")
240
+ elif avg_time < 5:
241
+ perf_text = Text("Performance: EXCELLENT - Ready for 240Hz+ gaming!", style="bold green")
242
+ elif avg_time < 10:
243
+ perf_text = Text("Performance: GREAT - Ready for 144Hz gaming!", style="bold cyan")
244
+ else:
245
+ perf_text = Text("Performance: GOOD - Suitable for standard gaming", style="bold yellow")
246
+ else:
247
+ perf_text = Text("⚠️ No test results parsed. Check your test suite.", style="bold red")
248
+
249
+ console.print(Align.center(Panel(perf_text, border_style="green")))
250
+ sys.exit(0 if failed == 0 else 1)
251
+
252
+ except ImportError:
253
+ print("📦 Rich not installed. Install it via `pip install rich` for enhanced output.")
254
+ print("\nFallback to raw pytest output...\n")
255
+
256
+ package_dir: Path = Path(__file__).resolve().parent
257
+ test_file: Path = package_dir / "test_suite.py"
258
+ html_file: Path = package_dir.parent / "latest_pytest.html"
259
+
260
+ result = pytest.main([
261
+ str(test_file),
262
+ "--rootdir", str(package_dir),
263
+ "-q",
264
+ "--tb=no",
265
+ "--html", str(html_file),
266
+ "--self-contained-html"
267
+ ])
268
+
269
+ # Try to parse HTML results even in fallback mode
270
+ try:
271
+ test_results, total_ms = parse_html_results(html_file)
272
+ passed = sum(1 for _, status, _ in test_results if status.upper() == "PASSED")
273
+ failed = sum(1 for _, status, _ in test_results if status.upper() == "FAILED")
274
+ skipped = sum(1 for _, status, _ in test_results if status.upper() == "SKIPPED")
275
+
276
+ print(f"\n📊 Results: {passed} passed, {failed} failed, {skipped} skipped")
277
+ if test_results:
278
+ avg_time = total_ms / len(test_results)
279
+ print(f"⏱️ Average time per test: {avg_time:.1f}ms")
280
+ except (FileNotFoundError, ValueError):
281
+ print("\n⚠️ Could not parse HTML results for summary")
282
+
283
+ if result != 0:
284
+ print("\n❌ Some tests failed.")
285
+ else:
286
+ print("\n✅ All tests passed.")
70
287
 
71
- sys.exit(result)
288
+ sys.exit(result)
72
289
 
73
- def main():
74
- args = sys.argv[1:]
290
+ def main() -> None:
291
+ args: List[str] = sys.argv[1:]
75
292
 
76
293
  if not args:
77
294
  print("Usage:")
makcu/conftest.py CHANGED
@@ -1,22 +1,32 @@
1
1
  import pytest
2
- from makcu import create_controller
3
2
  import time
3
+ from makcu import MakcuController, MouseButton
4
4
 
5
5
  @pytest.fixture(scope="session")
6
- def makcu():
7
- ctrl = create_controller()
8
- yield ctrl
9
- ctrl.disconnect()
10
- time.sleep(0.2)
6
+ def makcu(request):
7
+ """Session-scoped fixture with final cleanup at end of all tests"""
8
+ ctrl = MakcuController(fallback_com_port="COM1", debug=False)
11
9
 
12
- @pytest.fixture(autouse=True)
13
- def ensure_clean_exit(makcu):
14
- yield
15
- makcu.mouse.lock_left(False)
16
- makcu.mouse.lock_right(False)
17
- makcu.mouse.lock_middle(False)
18
- makcu.mouse.lock_side1(False)
19
- makcu.mouse.lock_side2(False)
20
- makcu.mouse.lock_x(False)
21
- makcu.mouse.lock_y(False)
22
- makcu.enable_button_monitoring(False)
10
+ def cleanup():
11
+ if ctrl.is_connected():
12
+ ctrl.lock_left(False)
13
+ ctrl.lock_right(False)
14
+ ctrl.lock_middle(False)
15
+ ctrl.lock_side1(False)
16
+ ctrl.lock_side2(False)
17
+ ctrl.lock_x(False)
18
+ ctrl.lock_y(False)
19
+
20
+ ctrl.release(MouseButton.LEFT)
21
+ ctrl.release(MouseButton.RIGHT)
22
+ ctrl.release(MouseButton.MIDDLE)
23
+ ctrl.release(MouseButton.MOUSE4)
24
+ ctrl.release(MouseButton.MOUSE5)
25
+
26
+ ctrl.enable_button_monitoring(False)
27
+
28
+ ctrl.disconnect()
29
+
30
+ request.addfinalizer(cleanup)
31
+
32
+ return ctrl