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: object

Configuration 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.

discovery_port

UDP port for discovery (default: 9999).

Type:

int

discovery_message

Message to send for discovery (default: b”DISCOVER_SERVER”).

Type:

bytes

response_prefix

Expected prefix in server responses (default: b”SERVER_IP:”).

Type:

bytes

timeout

Timeout in seconds for discovery operations (default: 5.0).

Type:

float

retries

Number of retry attempts (default: 3). Reserved for future use.

Type:

int

enable_subnet_scan

Whether to enable subnet scan (default: True). Reserved for future use.

Type:

bool

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

discovery_port: int = 9999
discovery_message: bytes
response_prefix: bytes
timeout: float = 5.0
retries: int = 3
enable_subnet_scan: bool = True
interfaces_whitelist: List[str] | None = None
interfaces_blacklist: List[str] | None = 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:

ClientConfig

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:

ClientConfig

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: object

A single discovered server returned by discover() or discover_one().

ip

IPv4 address of the server (e.g. “192.168.1.100”).

Type:

str

port

Port number (1-65535).

Type:

int

raw_response

Raw bytes of the server response (e.g. b”SERVER_IP:192.168.1.100:8000”).

Type:

bytes

extra

Optional metadata dict (e.g. source_address); default None.

Type:

Dict[str, Any] | None

Example

>>> r = DiscoveryResult(ip="192.168.1.100", port=8000, raw_response=b"SERVER_IP:...")
>>> print(r.ip, r.port)
ip: str
port: int
raw_response: bytes
extra: Dict[str, Any] | None = None

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: object

Information about a network interface.

name

Interface name (e.g., ‘eth0’, ‘en0’, ‘Ethernet’)

Type:

str

ip

IPv4 address as string (e.g., ‘192.168.1.100’)

Type:

str

netmask

Netmask as string (e.g., ‘255.255.255.0’)

Type:

str

broadcast

Broadcast address as string, or None if not available

Type:

str | None

name: str
ip: str
netmask: str
broadcast: str | None = None
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.

class discovery_client_django.apps.DiscoveryClientDjangoConfig(app_name, app_module)[source]

Bases: AppConfig

App configuration for discovery_client_django.

default_auto_field = 'django.db.models.BigAutoField'
name = 'discovery_client_django'
verbose_name = 'Django UDP Discovery Client'

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

class discovery_client_django.management.commands.discover_servers.Command(stdout=None, stderr=None, no_color=False, force_color=False)[source]

Bases: BaseCommand

Management command to discover django-udp-discovery servers.

help = 'Discover django-udp-discovery servers on the local network'
add_arguments(parser)[source]

Add command-line arguments.

handle(*args, **options)[source]

Execute the discovery command.