brownie1 0.1.0__tar.gz

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.
brownie1-0.1.0/.pypirc ADDED
@@ -0,0 +1,3 @@
1
+ [pypi]
2
+ username = __token__
3
+ password = pypi-AgEIcHlwaS5vcmcCJGY0ZTVlZTgxLWYwNWQtNGYyNi1hMjZmLWQwMDlkMmVmNzA1NAACKlszLCIyYmJhY2M0NC0zZmExLTQ0YjMtOTU1NC02NGE4YWYwMDgyN2UiXQAABiDRbuhaYK5SEK3XfvfIhrzB6AUDVRj21mRRtxBa5ajkWw
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.5
2
+ Name: brownie1
3
+ Version: 0.1.0
4
+ Summary: A minimalistic chat clone
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: bleak>=0.21.1
7
+ Requires-Dist: rich>=13.0.0
8
+ Requires-Dist: typer>=0.9.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.21.1; extra == 'dev'
11
+ Requires-Dist: pytest>=7.0; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # BlueChat Lite
15
+
16
+ BlueChat Lite is a minimalistic clone of BlueChat. It focuses purely on Bluetooth LE (BLE) client capabilities using `bleak`, acting as a central device that connects to Nordic UART Service (NUS) compatible peripherals.
17
+
18
+ It strips out the complex cryptographic authentication, file transfer chunks, and specific native GATT server implementations (like BlueZ, WinRT, and CoreBluetooth).
19
+
20
+ ## Features
21
+
22
+ - Scan for BLE devices (`bluechat-lite devices`)
23
+ - Connect and chat with a BLE device hosting Nordic UART Service (`bluechat-lite join <MAC_ADDRESS>`)
24
+ - Multiplatform client support (macOS, Linux, Windows) via `bleak`.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install -e .
30
+ ```
31
+
32
+ ## Testing
33
+
34
+ ```bash
35
+ pip install -e ".[dev]"
36
+ pytest
37
+ ```
@@ -0,0 +1,24 @@
1
+ # BlueChat Lite
2
+
3
+ BlueChat Lite is a minimalistic clone of BlueChat. It focuses purely on Bluetooth LE (BLE) client capabilities using `bleak`, acting as a central device that connects to Nordic UART Service (NUS) compatible peripherals.
4
+
5
+ It strips out the complex cryptographic authentication, file transfer chunks, and specific native GATT server implementations (like BlueZ, WinRT, and CoreBluetooth).
6
+
7
+ ## Features
8
+
9
+ - Scan for BLE devices (`bluechat-lite devices`)
10
+ - Connect and chat with a BLE device hosting Nordic UART Service (`bluechat-lite join <MAC_ADDRESS>`)
11
+ - Multiplatform client support (macOS, Linux, Windows) via `bleak`.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install -e .
17
+ ```
18
+
19
+ ## Testing
20
+
21
+ ```bash
22
+ pip install -e ".[dev]"
23
+ pytest
24
+ ```
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "brownie1"
7
+ version = "0.1.0"
8
+ description = "A minimalistic chat clone"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "bleak>=0.21.1",
13
+ "typer>=0.9.0",
14
+ "rich>=13.0.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=7.0",
20
+ "pytest-asyncio>=0.21.1",
21
+ ]
22
+
23
+ [tool.hatch.build.targets.sdist]
24
+ exclude = [
25
+ "/venv",
26
+ "/.venv",
27
+ "/dist",
28
+ "/build",
29
+ ]
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ only-include = ["src/brownie"]
33
+ sources = ["src"]
@@ -0,0 +1 @@
1
+ """Brownie Lite Package"""
@@ -0,0 +1,72 @@
1
+ import asyncio
2
+ import sys
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from brownie.core import scan_devices, BlueChatLiteClient
7
+
8
+ app = typer.Typer(help="Brownie Lite - Minimalistic BLE chat client")
9
+ console = Console()
10
+
11
+ @app.command("devices")
12
+ def list_devices(timeout: float = 5.0):
13
+ """Scan and list nearby BLE devices."""
14
+ console.print(f"Scanning for devices for {timeout} seconds...")
15
+
16
+ async def _scan():
17
+ devices = await scan_devices(timeout)
18
+ table = Table(title="Nearby BLE Devices")
19
+ table.add_column("Address", style="cyan")
20
+ table.add_column("Name", style="green")
21
+
22
+ for dev in devices:
23
+ table.add_row(dev.address, dev.name or "Unknown")
24
+
25
+ console.print(table)
26
+
27
+ asyncio.run(_scan())
28
+
29
+ @app.command("join")
30
+ def join(address: str):
31
+ """Connect to a BLE device by its MAC address."""
32
+ console.print(f"Connecting to {address}...")
33
+
34
+ async def _chat():
35
+ client = BlueChatLiteClient(address)
36
+
37
+ def handle_message(text: str):
38
+ # Print the received message to standard output using rich
39
+ console.print(f"\n[bold green]Peer:[/bold green] {text}")
40
+ # Reprint prompt since it got messed up
41
+ console.print("> ", end="")
42
+
43
+ client.on_message(handle_message)
44
+
45
+ try:
46
+ await client.connect()
47
+ console.print("[bold green]Connected! Type your messages (Ctrl+C to exit).[/bold green]")
48
+ except Exception as e:
49
+ console.print(f"[bold red]Failed to connect: {e}[/bold red]")
50
+ return
51
+
52
+ # Start a simple input loop
53
+ loop = asyncio.get_event_loop()
54
+ try:
55
+ while True:
56
+ # Use executor to not block asyncio event loop
57
+ msg = await loop.run_in_executor(None, input, "> ")
58
+ if msg.strip():
59
+ await client.send_message(msg)
60
+ except (KeyboardInterrupt, EOFError):
61
+ console.print("\nDisconnecting...")
62
+ finally:
63
+ await client.disconnect()
64
+ console.print("[bold green]Disconnected.[/bold green]")
65
+
66
+ try:
67
+ asyncio.run(_chat())
68
+ except KeyboardInterrupt:
69
+ sys.exit(0)
70
+
71
+ if __name__ == "__main__":
72
+ app()
@@ -0,0 +1,55 @@
1
+ import asyncio
2
+ from typing import Callable, List, Optional
3
+ from bleak import BleakScanner, BleakClient
4
+ from bleak.backends.device import BLEDevice
5
+
6
+ # Nordic UART Service UUIDs
7
+ NUS_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
8
+ NUS_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
9
+ NUS_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
10
+
11
+ async def scan_devices(timeout: float = 5.0) -> List[BLEDevice]:
12
+ """Scan for BLE devices."""
13
+ devices = await BleakScanner.discover(timeout=timeout)
14
+ return devices
15
+
16
+ class BlueChatLiteClient:
17
+ def __init__(self, address: str):
18
+ self.address = address
19
+ self.client: Optional[BleakClient] = None
20
+ self._message_callback: Optional[Callable[[str], None]] = None
21
+
22
+ def on_message(self, callback: Callable[[str], None]):
23
+ """Register a callback for incoming messages."""
24
+ self._message_callback = callback
25
+
26
+ async def connect(self):
27
+ """Connect to the BLE device."""
28
+ self.client = BleakClient(self.address)
29
+ await self.client.connect()
30
+ # Start notifying on the TX characteristic (device to host)
31
+ await self.client.start_notify(NUS_TX_UUID, self._notification_handler)
32
+
33
+ async def disconnect(self):
34
+ """Disconnect from the BLE device."""
35
+ if self.client and self.client.is_connected:
36
+ await self.client.stop_notify(NUS_TX_UUID)
37
+ await self.client.disconnect()
38
+
39
+ def _notification_handler(self, sender: int, data: bytearray):
40
+ """Handle incoming notifications (messages)."""
41
+ if self._message_callback:
42
+ try:
43
+ text = data.decode("utf-8")
44
+ self._message_callback(text)
45
+ except Exception as e:
46
+ print(f"[Error decoding message]: {e}")
47
+
48
+ async def send_message(self, message: str):
49
+ """Send a message to the BLE device."""
50
+ if not self.client or not self.client.is_connected:
51
+ raise RuntimeError("Not connected")
52
+
53
+ data = message.encode("utf-8")
54
+ # Send data to the RX characteristic (host to device)
55
+ await self.client.write_gatt_char(NUS_RX_UUID, data, response=True)
@@ -0,0 +1,79 @@
1
+ import pytest
2
+ import asyncio
3
+ from unittest.mock import AsyncMock, patch, MagicMock
4
+ from bluechat_lite.core import scan_devices, BlueChatLiteClient, NUS_RX_UUID, NUS_TX_UUID
5
+
6
+ @pytest.mark.asyncio
7
+ async def test_scan_devices():
8
+ """Test device scanning mock."""
9
+ mock_device = MagicMock()
10
+ mock_device.address = "00:11:22:33:44:55"
11
+ mock_device.name = "Test BLE Device"
12
+
13
+ with patch('bluechat_lite.core.BleakScanner.discover', new_callable=AsyncMock) as mock_discover:
14
+ mock_discover.return_value = [mock_device]
15
+
16
+ devices = await scan_devices(timeout=1.0)
17
+
18
+ mock_discover.assert_called_once_with(timeout=1.0)
19
+ assert len(devices) == 1
20
+ assert devices[0].address == "00:11:22:33:44:55"
21
+
22
+ @pytest.mark.asyncio
23
+ async def test_client_connect_and_disconnect():
24
+ """Test connecting and disconnecting."""
25
+ with patch('bluechat_lite.core.BleakClient') as MockClient:
26
+ mock_client_instance = MockClient.return_value
27
+ mock_client_instance.connect = AsyncMock()
28
+ mock_client_instance.start_notify = AsyncMock()
29
+ mock_client_instance.stop_notify = AsyncMock()
30
+ mock_client_instance.disconnect = AsyncMock()
31
+ mock_client_instance.is_connected = True
32
+
33
+ client = BlueChatLiteClient("00:11:22:33:44:55")
34
+
35
+ await client.connect()
36
+ mock_client_instance.connect.assert_called_once()
37
+ mock_client_instance.start_notify.assert_called_once_with(NUS_TX_UUID, client._notification_handler)
38
+
39
+ await client.disconnect()
40
+ mock_client_instance.stop_notify.assert_called_once_with(NUS_TX_UUID)
41
+ mock_client_instance.disconnect.assert_called_once()
42
+
43
+ @pytest.mark.asyncio
44
+ async def test_client_send_and_receive_messages():
45
+ """Test sending a message and receiving a notification."""
46
+ with patch('bluechat_lite.core.BleakClient') as MockClient:
47
+ mock_client_instance = MockClient.return_value
48
+ mock_client_instance.connect = AsyncMock()
49
+ mock_client_instance.start_notify = AsyncMock()
50
+ mock_client_instance.write_gatt_char = AsyncMock()
51
+ mock_client_instance.is_connected = True
52
+
53
+ client = BlueChatLiteClient("00:11:22:33:44:55")
54
+
55
+ received_messages = []
56
+ def handler(msg):
57
+ received_messages.append(msg)
58
+
59
+ client.on_message(handler)
60
+
61
+ await client.connect()
62
+
63
+ # Test receiving a message
64
+ client._notification_handler(123, b"Hello World")
65
+ assert len(received_messages) == 1
66
+ assert received_messages[0] == "Hello World"
67
+
68
+ # Test sending a message
69
+ await client.send_message("Hello back")
70
+ mock_client_instance.write_gatt_char.assert_called_once_with(NUS_RX_UUID, b"Hello back", response=True)
71
+
72
+ @pytest.mark.asyncio
73
+ async def test_client_send_disconnected_raises_error():
74
+ """Test sending when disconnected raises error."""
75
+ client = BlueChatLiteClient("00:11:22:33:44:55")
76
+ # Client is not connected
77
+
78
+ with pytest.raises(RuntimeError, match="Not connected"):
79
+ await client.send_message("Failed message")
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Ensure we are in the project root
5
+ cd "$(dirname "$0")"
6
+
7
+ echo "Activating virtual environment if it exists..."
8
+ if [ -d "venv" ]; then
9
+ source venv/bin/activate
10
+ fi
11
+
12
+ echo "Installing build and twine..."
13
+ pip install --upgrade build twine
14
+
15
+ echo "Cleaning up previous builds..."
16
+ rm -rf dist/
17
+
18
+ echo "Building the package..."
19
+ python3 -m build
20
+
21
+ echo "Uploading to PyPI..."
22
+ echo "You will be prompted for your PyPI API token (username: __token__)"
23
+ python3 -m twine upload dist/*
24
+
25
+ echo "Done!"