django-udp-discovery-client modules
High-level API
These callables are re-exported from discovery_client (package root).
- discovery_client.discover(config: ClientConfig | None = None) List[DiscoveryResult][source]
Discover django-udp-discovery servers on the local network.
Sends UDP discovery requests (DISCOVER_SERVER) to the network and collects responses from servers that respond with the SERVER_IP: prefix. Uses multi-interface broadcast; results are deduplicated by (ip, port).
- Parameters:
config – Optional ClientConfig. If None, uses load_config() (defaults + env).
- Returns:
One entry per discovered server (ip, port, raw_response, extra). Empty list if no servers are found, timeout, or on network/import errors (logged).
- Return type:
List[DiscoveryResult]
Example
>>> from discovery_client import discover, load_config >>> servers = discover() >>> for s in servers: ... print(s.ip, s.port) # DiscoveryResult attributes >>> servers = discover(config=load_config(timeout=10.0))
- discovery_client.discover_one(config: ClientConfig | None = None) DiscoveryResult | None[source]
Discover a single django-udp-discovery server (first respondent).
Convenience wrapper around discover(): returns the first DiscoveryResult or None if no servers are found. Same protocol and config as discover().
- Parameters:
config – Optional ClientConfig. If None, uses load_config() (defaults + env).
- Returns:
First discovered server (ip, port, raw_response, extra), or None if none found or on error.
- Return type:
Optional[DiscoveryResult]
Example
>>> from discovery_client import discover_one >>> server = discover_one() >>> if server: ... print(server.ip, server.port)
Configuration
Configuration module for UDP discovery client.
Provides ClientConfig dataclass with defaults, runtime overrides, and environment variable support.
- class discovery_client.config.ClientConfig(discovery_port: int = 9999, discovery_message: bytes = <factory>, response_prefix: bytes = <factory>, timeout: float = 5.0, retries: int = 3, enable_subnet_scan: bool = True, interfaces_whitelist: ~typing.List[str] | None = None, interfaces_blacklist: ~typing.List[str] | None = None)[source]
Bases:
objectConfiguration for UDP discovery client.
Supports built-in defaults, environment variables (DISCOVERY_CLIENT_*), and runtime overrides. When using load_config() or from_env(), priority is: defaults < env vars < keyword overrides.
- enable_subnet_scan
Whether to enable subnet scan (default: True). Reserved for future use.
- Type:
- interfaces_whitelist
If set, only these interface names are used (default: None).
- Type:
List[str] | None
- interfaces_blacklist
If set, these interface names are excluded (default: None).
- Type:
List[str] | None
- classmethod from_env(**overrides: object) ClientConfig[source]
Create ClientConfig from environment variables with optional overrides.
Priority: built-in defaults < DISCOVERY_CLIENT_* env vars < **overrides. Only keys not present in overrides are read from the environment.
- Environment variables (all optional, prefix DISCOVERY_CLIENT_):
PORT: Discovery port (int) MESSAGE: Discovery message (str/bytes) RESPONSE_PREFIX: Response prefix (str/bytes) TIMEOUT: Timeout in seconds (float) RETRIES: Number of retries (int) ENABLE_SUBNET_SCAN: Enable subnet scan (bool: true/1/yes/on) INTERFACES_WHITELIST: Comma-separated interface names INTERFACES_BLACKLIST: Comma-separated interface names
- Parameters:
**overrides – Runtime overrides; take precedence over env vars and defaults.
- Returns:
New instance with merged configuration.
- Return type:
- discovery_client.config.load_config(**kwargs: object) ClientConfig[source]
Load and validate ClientConfig with optional overrides.
Priority order (highest wins): (1) keyword arguments, (2) environment variables (DISCOVERY_CLIENT_*), (3) built-in defaults. So load_config() uses defaults plus any DISCOVERY_CLIENT_* env vars; load_config(timeout=5) uses 5 for timeout and env/defaults for the rest.
- Parameters:
**kwargs – Optional keyword overrides (e.g. timeout=10.0, discovery_port=8888). These take precedence over environment variables and defaults.
- Returns:
Validated configuration instance.
- Return type:
Example
>>> config = load_config(timeout=10.0, discovery_port=8888) >>> config = load_config() # Uses defaults and env vars only
Results
Discovery result data structures.
Defines DiscoveryResult, the type returned by discover() and discover_one().
- class discovery_client.results.DiscoveryResult(ip: str, port: int, raw_response: bytes, extra: Dict[str, Any] | None = None)[source]
Bases:
objectA single discovered server returned by discover() or discover_one().
Example
>>> r = DiscoveryResult(ip="192.168.1.100", port=8000, raw_response=b"SERVER_IP:...") >>> print(r.ip, r.port)
Network interfaces
Network interface enumeration for discovery_client.
Enumerates active IPv4 non-loopback interfaces (IP, netmask, broadcast), via netifaces or ifaddr. Provides InterfaceInfo and select_interfaces() for whitelist/blacklist filtering. Required for multi-interface discovery.
- class discovery_client.network.interfaces.InterfaceInfo(name: str, ip: str, netmask: str, broadcast: str | None = None)[source]
Bases:
objectInformation about a network interface.
- discovery_client.network.interfaces.get_interfaces() List[InterfaceInfo][source]
Enumerate all active IPv4 network interfaces.
Returns a list of InterfaceInfo objects for each active non-loopback interface. Broadcast addresses are computed if not provided by the system.
- Returns:
List of InterfaceInfo objects
- Raises:
ImportError – If neither netifaces nor ifaddr is available
Example
>>> interfaces = get_interfaces() >>> for iface in interfaces: ... print(f"{iface.name}: {iface.ip}/{iface.netmask} -> {iface.broadcast}")
- discovery_client.network.interfaces.select_interfaces(config: ClientConfig) List[InterfaceInfo][source]
Select and filter network interfaces based on ClientConfig whitelist/blacklist.
Filters the list of available network interfaces according to the whitelist and blacklist settings in the provided ClientConfig.
Filtering rules: 1. Start with all non-loopback interfaces from get_interfaces() 2. If whitelist is set, keep only interfaces whose name is in the whitelist 3. If blacklist is set, remove interfaces whose name is in the blacklist 4. If both whitelist and blacklist are set, whitelist is applied first, then blacklist
Interface name matching is case-sensitive and exact (e.g., “eth0” != “Eth0”).
- Parameters:
config – ClientConfig instance containing interfaces_whitelist and/or interfaces_blacklist settings
- Returns:
List of InterfaceInfo objects that pass the filtering criteria. Returns empty list if no interfaces match the criteria.
- Raises:
ImportError – If neither netifaces nor ifaddr is available (from get_interfaces)
Example
>>> from discovery_client import ClientConfig, load_config >>> from discovery_client.network.interfaces import select_interfaces >>> >>> # Whitelist only specific interfaces >>> config = ClientConfig(interfaces_whitelist=["eth0", "wlan0"]) >>> interfaces = select_interfaces(config) >>> >>> # Blacklist specific interfaces >>> config = ClientConfig(interfaces_blacklist=["lo", "docker0"]) >>> interfaces = select_interfaces(config) >>> >>> # Both whitelist and blacklist >>> config = ClientConfig( ... interfaces_whitelist=["eth0", "eth1", "wlan0"], ... interfaces_blacklist=["eth1"] ... ) >>> interfaces = select_interfaces(config) >>> # Result: only eth0 and wlan0 (eth1 is blacklisted even though whitelisted)
Socket layer
UDP socket layer for discovery_client.
Low-level operations: create broadcast socket, send discovery message, receive and parse SERVER_IP: responses, multi-interface broadcast discovery, segmented-network detection, and formatted warning message for callers.
- discovery_client.network.socket.parse_response(response: bytes, prefix: bytes) Tuple[str, int] | None[source]
Parse a discovery response to extract IP and port.
Expected format: “SERVER_IP:<ip>[:port]” .. rubric:: Examples
“SERVER_IP:192.168.1.100:8000” -> (“192.168.1.100”, 8000)
“SERVER_IP:192.168.1.100” -> (“192.168.1.100”, 8000) # default port
- Parameters:
response – Raw bytes received from server
prefix – Expected response prefix (e.g., b”SERVER_IP:”)
- Returns:
Tuple of (ip, port) if parsing succeeds, None otherwise. Port defaults to 8000 if not specified in response.
Example
>>> parse_response(b"SERVER_IP:192.168.1.100:8000", b"SERVER_IP:") ('192.168.1.100', 8000) >>> parse_response(b"SERVER_IP:10.0.0.5", b"SERVER_IP:") ('10.0.0.5', 8000)
- discovery_client.network.socket.create_discovery_socket(timeout: float) socket[source]
Create and configure a UDP socket for discovery.
- Parameters:
timeout – Socket timeout in seconds
- Returns:
Configured UDP socket with broadcast enabled
- Raises:
OSError – If socket creation or configuration fails
- discovery_client.network.socket.send_discovery_request(sock: socket, message: bytes, port: int, broadcast_address: str = DEFAULT_BROADCAST_ADDRESS) None[source]
Send a UDP discovery request to the broadcast address.
- Parameters:
sock – UDP socket (must have SO_BROADCAST enabled)
message – Discovery message to send (e.g., b”DISCOVER_SERVER”)
port – UDP port to send to
broadcast_address – Broadcast address (default: “255.255.255.255”)
- Raises:
OSError – If send fails
- discovery_client.network.socket.receive_responses(sock: socket, config: ClientConfig, max_responses: int | None = None) List[DiscoveryResult][source]
Receive and parse discovery responses until timeout.
- Parameters:
sock – UDP socket with timeout set
config – ClientConfig with response_prefix for parsing
max_responses – Maximum number of responses to collect (None = unlimited)
- Returns:
List of DiscoveryResult objects for valid responses
Note
This function will continue receiving until socket timeout. Invalid responses (wrong prefix) are silently ignored.
- discovery_client.network.socket.discover_servers_single_broadcast(config: ClientConfig, broadcast_address: str = DEFAULT_BROADCAST_ADDRESS) List[DiscoveryResult][source]
Perform UDP discovery using a single broadcast address.
This is the core discovery function that: 1. Creates a UDP socket with broadcast enabled 2. Sends discovery message to broadcast address 3. Receives and parses responses until timeout 4. Returns list of discovered servers
- Parameters:
config – ClientConfig with discovery settings
broadcast_address – Broadcast address to use (default: “255.255.255.255”)
- Returns:
List of DiscoveryResult objects for discovered servers. Returns empty list if no servers respond or timeout occurs.
- Raises:
OSError – If socket operations fail
Example
>>> from discovery_client import ClientConfig >>> config = ClientConfig(timeout=5.0) >>> servers = discover_servers_single_broadcast(config) >>> for server in servers: ... print(f"Found: {server.ip}:{server.port}")
- discovery_client.network.socket.get_interface_broadcast(iface: InterfaceInfo) str[source]
Get broadcast address for an interface.
Uses the interface’s broadcast address if present, otherwise computes it from the interface’s IP and netmask.
- Parameters:
iface – InterfaceInfo object
- Returns:
Broadcast address as string
- Raises:
ValueError – If broadcast cannot be computed (invalid IP/netmask)
- discovery_client.network.socket.detect_segmented_network(interfaces: List[InterfaceInfo]) dict | None[source]
Detect if the network is a large segmented network that may not work with current broadcast-based discovery.
Large subnets (prefix < 24) are often segmented into /24 VLANs, and UDP broadcasts only reach devices in the same broadcast domain. This function detects such networks and returns information about the issue.
This function is OS-independent and uses only standard Python libraries: - ipaddress module (Python 3.3+ standard library) - Standard IP address parsing and network calculations - Works on Windows, Linux, macOS, and other platforms
Detection criteria (all must be true to trigger warning): 1. Network prefix < 24 (large subnet) 2. Network is in corporate range (10.x.x.x or 172.16-31.x.x) 3. Broadcast domain differs from calculated broadcast (indicates segmentation)
Networks that are NOT flagged: - /24 networks (typical for mobile hotspots, home networks) - these work fine - 192.168.x.x networks with /24 or smaller - typically not segmented - Networks where broadcast domain matches calculated broadcast
- Parameters:
interfaces – List of InterfaceInfo objects to check
- Returns:
Dictionary with detection results if segmented network detected, None otherwise. Contains keys: ‘interface’, ‘ip’, ‘network’, ‘prefix’, ‘total_hosts’, ‘calculated_broadcast’, ‘likely_broadcast_domain’, ‘is_corporate’, ‘segments’
Note
Detection is based on network topology (subnet size and IP ranges), not OS-specific features. Works identically across all platforms.
- discovery_client.network.socket.format_segmented_network_warning(segmented_info: dict, width: int = 40) str[source]
Format the “Segmented Network Detected” diagnostic message for display. Used by management command and sanity_check to print exactly once per run.
- discovery_client.network.socket.deduplicate_results(results: List[DiscoveryResult]) List[DiscoveryResult][source]
Remove duplicate DiscoveryResult objects based on (ip, port) key.
Keeps the first occurrence of each unique (ip, port) combination.
- Parameters:
results – List of DiscoveryResult objects (may contain duplicates)
- Returns:
port)
- Return type:
List of unique DiscoveryResult objects (no duplicates by ip
Example
>>> results = [ ... DiscoveryResult(ip="192.168.1.100", port=8000, raw_response=b"response1"), ... DiscoveryResult(ip="192.168.1.100", port=8000, raw_response=b"response2"), ... DiscoveryResult(ip="10.0.0.5", port=9000, raw_response=b"response3"), ... ] >>> unique = deduplicate_results(results) >>> len(unique) 2
- discovery_client.network.socket.discover_servers_multi_interface(config: ClientConfig) List[DiscoveryResult][source]
Perform UDP discovery using multiple network interfaces.
This function: 1. Selects interfaces based on config (whitelist/blacklist) 2. Sends discovery messages to each interface’s broadcast address 3. Receives responses from all interfaces on a single socket 4. Deduplicates results by (ip, port)
- Parameters:
config – ClientConfig with discovery settings and interface filters
- Returns:
List of unique DiscoveryResult objects for discovered servers. Returns empty list if no servers respond or timeout occurs.
- Raises:
OSError – If socket operations fail
ImportError – If network interface libraries are not available
Example
>>> from discovery_client import ClientConfig >>> config = ClientConfig(timeout=5.0) >>> servers = discover_servers_multi_interface(config) >>> for server in servers: ... print(f"Found: {server.ip}:{server.port}")
Network utilities
Network utility functions for netmask and network calculations.
Provides helpers for netmask-to-prefix conversion, computing network ranges, and calculating broadcast addresses.
- discovery_client.network.utils.netmask_to_prefix(netmask: str) int[source]
Convert netmask to CIDR prefix length.
Converts a netmask in dotted decimal notation (e.g., “255.255.255.0”) to a CIDR prefix length (e.g., 24).
- Parameters:
netmask – Netmask as string in dotted decimal format (e.g., “255.255.255.0”)
- Returns:
CIDR prefix length as integer (0-32)
- Raises:
ValueError – If netmask is invalid or non-contiguous
Example
>>> netmask_to_prefix("255.255.255.0") 24 >>> netmask_to_prefix("255.0.0.0") 8
- discovery_client.network.utils.prefix_to_netmask(prefix: int) str[source]
Convert CIDR prefix length to netmask.
Converts a CIDR prefix length (e.g., 24) to a netmask in dotted decimal notation (e.g., “255.255.255.0”).
- Parameters:
prefix – CIDR prefix length (0-32)
- Returns:
Netmask as string in dotted decimal format
- Raises:
ValueError – If prefix is out of valid range (0-32)
Example
>>> prefix_to_netmask(24) '255.255.255.0' >>> prefix_to_netmask(8) '255.0.0.0'
- discovery_client.network.utils.network_from_ip_and_mask(ip: str, mask: str | int) IPv4Network[source]
Create IPv4Network object from IP address and netmask.
- Parameters:
ip – IPv4 address as string (e.g., “192.168.1.100”)
mask – Netmask as string (e.g., “255.255.255.0”) or prefix length as int (e.g., 24)
- Returns:
IPv4Network object representing the network
- Raises:
ValueError – If IP or mask is invalid
Example
>>> network = network_from_ip_and_mask("192.168.1.100", "255.255.255.0") >>> str(network) '192.168.1.0/24' >>> network = network_from_ip_and_mask("10.0.0.1", 8) >>> str(network) '10.0.0.0/8'
- discovery_client.network.utils.broadcast_from_ip_and_mask(ip: str, mask: str | int) str[source]
Calculate broadcast address from IP address and netmask.
- Parameters:
ip – IPv4 address as string (e.g., “192.168.1.100”)
mask – Netmask as string (e.g., “255.255.255.0”) or prefix length as int (e.g., 24)
- Returns:
Broadcast address as string
- Raises:
ValueError – If IP or mask is invalid
Example
>>> broadcast_from_ip_and_mask("192.168.1.100", "255.255.255.0") '192.168.1.255' >>> broadcast_from_ip_and_mask("10.0.0.1", 8) '10.255.255.255'
Django helper app
Django app configuration for discovery_client_django.
Management command
Django management command to discover django-udp-discovery servers.
- Usage:
python manage.py discover_servers python manage.py discover_servers –timeout 10.0 python manage.py discover_servers –port 9999