Skip to content

Commit a9e8434

Browse files
author
Alexander Brinkman
committed
Add PyPI
1 parent 2af0fc7 commit a9e8434

7 files changed

Lines changed: 158 additions & 71 deletions

File tree

.github/workflows/publish.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v4
19+
with:
20+
python-version: "3.14"
21+
22+
- name: Install build dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install build twine
26+
27+
- name: Verify tag format
28+
run: |
29+
if [[ ! "${{ github.ref }}" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
30+
echo "Tag does not match version format (v*.*.*)"
31+
exit 1
32+
fi
33+
34+
- name: Build distribution
35+
run: python -m build
36+
37+
- name: Check distribution
38+
run: twine check dist/*
39+
40+
- name: Publish to PyPI
41+
uses: pypa/gh-action-pypi-publish@release/v1
42+
with:
43+
password: ${{ secrets.PYPI_API_TOKEN }}

binding_configuration.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ def _load_config(self) -> ET.Element:
6666
tree = ET.parse(self.config_path)
6767
return tree.getroot()
6868

69-
def add_binding(
70-
self, vendor_id: str, product_id: str, serial_number: str = ""
71-
) -> bool:
69+
def add_binding(self, vendor_id: str, product_id: str, serial_number: str = "") -> bool:
7270
"""
7371
Add a device binding to the configuration.
7472
@@ -107,9 +105,7 @@ def add_binding(
107105
self._write_config(root)
108106
return True
109107

110-
def remove_binding(
111-
self, vendor_id: str, product_id: str, serial_number: str = ""
112-
) -> bool:
108+
def remove_binding(self, vendor_id: str, product_id: str, serial_number: str = "") -> bool:
113109
"""
114110
Remove a device binding from the configuration.
115111
@@ -182,14 +178,9 @@ def get_all_bindings(self) -> list[dict]:
182178
if bindings is None:
183179
return []
184180

185-
return [
186-
self._device_element_to_dict(device)
187-
for device in bindings.findall("device")
188-
]
181+
return [self._device_element_to_dict(device) for device in bindings.findall("device")]
189182

190-
def is_bound(
191-
self, vendor_id: str, product_id: str, serial_number: str = ""
192-
) -> bool:
183+
def is_bound(self, vendor_id: str, product_id: str, serial_number: str = "") -> bool:
193184
"""
194185
Check if a device is bound.
195186

libusb_backend.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111
import sys
1212
from typing import Any
1313

14-
import usb.backend.libusb1 as libusb1
15-
1614
# Import libusb to ensure it's installed (it's a required dependency)
1715
import libusb
16+
import usb.backend.libusb1 as libusb1
1817

1918

2019
def _get_bundled_libusb_path() -> str:

pyproject.toml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0", "wheel", "setuptools-scm>=8.0"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "usbipd-python"
7+
dynamic = ["version"]
8+
description = "A USB/IP server for sharing USB devices over the network"
9+
readme = "README.md"
10+
requires-python = ">=3.9"
11+
license = {text = "GPL-3.0"}
12+
authors = [
13+
{name = "abrinkman", email = "abrinkman@github.com"},
14+
]
15+
keywords = ["usb", "usbip", "network", "device-sharing"]
16+
classifiers = [
17+
"Programming Language :: Python :: 3",
18+
"Programming Language :: Python :: 3.9",
19+
"Programming Language :: Python :: 3.10",
20+
"Programming Language :: Python :: 3.11",
21+
"Programming Language :: Python :: 3.12",
22+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
23+
"Operating System :: MacOS",
24+
"Operating System :: POSIX :: Linux",
25+
"Topic :: System :: Networking",
26+
"Development Status :: 3 - Alpha",
27+
"Intended Audience :: Developers",
28+
"Intended Audience :: System Administrators",
29+
]
30+
31+
dependencies = [
32+
"pyusb>=1.2.1",
33+
]
34+
35+
[project.urls]
36+
Homepage = "https://github.com/abrinkman/usbipd-python"
37+
Repository = "https://github.com/abrinkman/usbipd-python.git"
38+
Issues = "https://github.com/abrinkman/usbipd-python/issues"
39+
40+
[project.scripts]
41+
usbipd-python = "usbipd:main"
42+
43+
[tool.setuptools]
44+
py-modules = ["usbipd", "usb_device", "usbip_server", "binding_configuration", "libusb_backend"]
45+
46+
[tool.ruff]
47+
target-version = "py39"
48+
line-length = 100
49+
50+
[tool.ruff.lint]
51+
select = ["E", "F", "W", "I", "N", "UP"]
52+
ignore = [
53+
"E501",
54+
"N806", # USB spec variable names like bmRequestType, wValue, etc.
55+
]
56+
57+
[tool.mypy]
58+
python_version = "3.9"
59+
warn_return_any = true
60+
warn_unused_configs = true
61+
ignore_missing_imports = true
62+
63+
[tool.setuptools_scm]
64+
# Version is derived from git tags (e.g., v0.1.0 -> 0.1.0)
65+
# If no tag, generates dev version like 0.1.0.dev1+g1234567

usb_device.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import logging
1111
import re
12-
from typing import Dict, List, Optional, Tuple
12+
from typing import Optional
1313

1414
import usb.core
1515
import usb.util
@@ -78,7 +78,7 @@ def build_bus_id(device: usb.core.Device) -> str:
7878
return f"{device.bus}-{device.address}"
7979

8080
@staticmethod
81-
def parse_bus_id(bus_id: str) -> Tuple[int, Tuple[int, ...]]:
81+
def parse_bus_id(bus_id: str) -> tuple[int, tuple[int, ...]]:
8282
"""Parse a bus ID string into bus number and port numbers.
8383
8484
Args:
@@ -113,9 +113,7 @@ def _load_strings(self) -> None:
113113

114114
try:
115115
if self.device.iManufacturer:
116-
raw_manufacturer = usb.util.get_string(
117-
self.device, self.device.iManufacturer
118-
)
116+
raw_manufacturer = usb.util.get_string(self.device, self.device.iManufacturer)
119117
self._manufacturer = self.clean_usb_string(raw_manufacturer)
120118
except (usb.core.USBError, ValueError) as error:
121119
logger.debug("Could not read manufacturer string: %s", error)
@@ -137,12 +135,12 @@ def _load_strings(self) -> None:
137135
@property
138136
def vendor_id(self) -> int:
139137
"""Get the vendor ID (VID) of the device."""
140-
return self.device.idVendor
138+
return int(self.device.idVendor)
141139

142140
@property
143141
def product_id(self) -> int:
144142
"""Get the product ID (PID) of the device."""
145-
return self.device.idProduct
143+
return int(self.device.idProduct)
146144

147145
@property
148146
def manufacturer(self) -> Optional[str]:
@@ -170,7 +168,7 @@ def device_id(self) -> str:
170168
return f"{self.vendor_id:04x}:{self.product_id:04x}:{self._serial_number}"
171169
return f"{self.vendor_id:04x}:{self.product_id:04x}"
172170

173-
def to_dict(self) -> Dict[str, Optional[str]]:
171+
def to_dict(self) -> dict[str, Optional[str]]:
174172
"""Get basic device information as a dictionary.
175173
176174
Returns:
@@ -259,19 +257,15 @@ def claim(self) -> bool:
259257
access_denied = True
260258
logger.debug("Access denied for interface %d", interface_number)
261259
else:
262-
logger.warning(
263-
"Could not claim interface %d: %s", interface_number, error
264-
)
260+
logger.warning("Could not claim interface %d: %s", interface_number, error)
265261

266262
except usb.core.USBError as error:
267263
if error.errno == 13:
268264
access_denied = True
269265
logger.warning("Error claiming device: %s", error)
270266

271267
if access_denied:
272-
logger.error(
273-
"Insufficient permissions to access USB device. Try running with sudo."
274-
)
268+
logger.error("Insufficient permissions to access USB device. Try running with sudo.")
275269
return False
276270

277271
return True
@@ -291,9 +285,7 @@ def release(self) -> None:
291285
usb.util.release_interface(self.device, interface_number)
292286
logger.debug("Released interface %d", interface_number)
293287
except usb.core.USBError as error:
294-
logger.debug(
295-
"Could not release interface %d: %s", interface_number, error
296-
)
288+
logger.debug("Could not release interface %d: %s", interface_number, error)
297289
except usb.core.USBError as error:
298290
logger.debug("Could not get configuration for release: %s", error)
299291

@@ -363,7 +355,7 @@ def __init__(self) -> None:
363355
"""Initialize the USBDeviceManager."""
364356
self._logger = logging.getLogger(__name__)
365357

366-
def list_devices(self) -> List[USBDevice]:
358+
def list_devices(self) -> list[USBDevice]:
367359
"""List all available USB devices.
368360
369361
Returns:
@@ -443,7 +435,7 @@ def find_by_identity(
443435

444436
return None
445437

446-
def find_by_binding(self, binding: Dict[str, str]) -> Optional[USBDevice]:
438+
def find_by_binding(self, binding: dict[str, str]) -> Optional[USBDevice]:
447439
"""Find a device that matches a binding configuration.
448440
449441
The binding dictionary should contain 'vendor_id', 'product_id',

usbip_server.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __init__(self, host: str = "::", port: int = DEFAULT_PORT) -> None:
6565
self.port = port
6666
self._server_socket: Optional[socket.socket] = None
6767
self._running = False
68-
self._exported_devices: dict[str, "USBDevice"] = {}
68+
self._exported_devices: dict[str, USBDevice] = {}
6969
self._active_connections: list[threading.Thread] = []
7070
self._lock = threading.Lock()
7171

@@ -155,9 +155,7 @@ def stop(self) -> None:
155155
self._server_socket = None
156156
logger.info("USB/IP server stopped")
157157

158-
def _handle_client(
159-
self, client_socket: socket.socket, client_address: tuple
160-
) -> None:
158+
def _handle_client(self, client_socket: socket.socket, client_address: tuple) -> None:
161159
"""
162160
Handle a client connection.
163161
@@ -260,7 +258,7 @@ def _build_device_info(self, bus_id: str, device: usb.core.Device) -> bytes:
260258
The packed device information bytes.
261259
"""
262260
# Path (256 bytes, zero-padded)
263-
path = f"/sys/devices/usb/{bus_id}".encode("utf-8")
261+
path = f"/sys/devices/usb/{bus_id}".encode()
264262
path = path[:255] + b"\x00" * (256 - len(path))
265263

266264
# Bus ID (32 bytes, zero-padded)
@@ -402,7 +400,7 @@ def _build_import_device_info(self, bus_id: str, device: usb.core.Device) -> byt
402400
The packed device information bytes.
403401
"""
404402
# Path (256 bytes, zero-padded)
405-
path = f"/sys/devices/usb/{bus_id}".encode("utf-8")
403+
path = f"/sys/devices/usb/{bus_id}".encode()
406404
path = path[:255] + b"\x00" * (256 - len(path))
407405

408406
# Bus ID (32 bytes, zero-padded)
@@ -462,9 +460,7 @@ def _handle_urb_traffic(self, client_socket: socket.socket, bus_id: str) -> None
462460

463461
# Claim the device for exclusive access
464462
if not usb_device.claim():
465-
logger.error(
466-
f"Cannot handle URB traffic for {bus_id} - device claim failed"
467-
)
463+
logger.error(f"Cannot handle URB traffic for {bus_id} - device claim failed")
468464
return
469465

470466
logger.info(f"Starting URB traffic handling for {bus_id}")
@@ -548,9 +544,7 @@ def _handle_urb_submit(
548544
is_device_to_host = (bmRequestType & 0x80) != 0
549545
if not is_device_to_host:
550546
# Host to Device (OUT) - read the data
551-
recv_result = self._recv_exact(
552-
client_socket, transfer_buffer_length
553-
)
547+
recv_result = self._recv_exact(client_socket, transfer_buffer_length)
554548
if recv_result is None:
555549
return
556550
transfer_buffer = recv_result
@@ -642,9 +636,7 @@ def _do_control_transfer(
642636
Returns:
643637
A tuple of (response_data, actual_length).
644638
"""
645-
bmRequestType, bRequest, wValue, wIndex, wLength = struct.unpack(
646-
"<BBHHH", setup
647-
)
639+
bmRequestType, bRequest, wValue, wIndex, wLength = struct.unpack("<BBHHH", setup)
648640

649641
# Direction is encoded in bmRequestType bit 7:
650642
# 0 = Host to Device (OUT), 1 = Device to Host (IN)
@@ -727,9 +719,7 @@ def _do_bulk_interrupt_transfer(
727719
result = device.write(endpoint_addr, data, timeout=5000)
728720
return b"", result
729721

730-
def _handle_urb_unlink(
731-
self, client_socket: socket.socket, header: bytes, seqnum: int
732-
) -> None:
722+
def _handle_urb_unlink(self, client_socket: socket.socket, header: bytes, seqnum: int) -> None:
733723
"""
734724
Handle USBIP_CMD_UNLINK command.
735725

0 commit comments

Comments
 (0)