Simple DNS resolver for asyncio
npx @tessl/cli install tessl/pypi-aiodns@3.5.00
# aiodns
1
2
Simple DNS resolver for asyncio that provides asynchronous DNS resolution capabilities using the pycares library. aiodns enables non-blocking DNS queries in Python applications using async/await syntax, supporting all major DNS record types and offering efficient hostname resolution with /etc/hosts support.
3
4
## Package Information
5
6
- **Package Name**: aiodns
7
- **Version**: 3.5.0
8
- **Language**: Python
9
- **Installation**: `pip install aiodns`
10
- **Dependencies**: `pycares>=4.9.0`
11
- **Python Version**: 3.9+
12
13
## Core Imports
14
15
```python
16
import aiodns
17
```
18
19
Common usage pattern:
20
21
```python
22
from aiodns import DNSResolver
23
```
24
25
Access to error constants and types:
26
27
```python
28
from aiodns import error
29
from aiodns.error import DNSError, ARES_ENOTFOUND, ARES_ETIMEOUT
30
import socket
31
import asyncio
32
from types import TracebackType
33
from typing import Optional, Sequence, Union, Iterable, Literal, Any
34
import pycares
35
```
36
37
## Basic Usage
38
39
```python
40
import asyncio
41
import aiodns
42
43
async def main():
44
# Create a DNS resolver
45
resolver = aiodns.DNSResolver()
46
47
try:
48
# Perform A record query (returns list[pycares.ares_query_a_result])
49
a_result = await resolver.query('google.com', 'A')
50
print(f"A records: {[r.host for r in a_result]}")
51
52
# Perform AAAA record query (returns list[pycares.ares_query_aaaa_result])
53
aaaa_result = await resolver.query('google.com', 'AAAA')
54
print(f"IPv6 addresses: {[r.host for r in aaaa_result]}")
55
56
# CNAME query returns single result (not a list)
57
cname_result = await resolver.query('www.github.com', 'CNAME')
58
print(f"CNAME: {cname_result.cname}")
59
60
# MX query returns list of results
61
mx_result = await resolver.query('github.com', 'MX')
62
print(f"MX records: {[(r.host, r.priority) for r in mx_result]}")
63
64
# Hostname resolution with /etc/hosts support
65
import socket
66
host_result = await resolver.gethostbyname('localhost', socket.AF_INET)
67
print(f"Localhost IP: {host_result.addresses}")
68
69
except aiodns.error.DNSError as e:
70
print(f"DNS error: {e}")
71
finally:
72
# Clean up resources
73
await resolver.close()
74
75
# Using async context manager (automatic cleanup)
76
async def context_example():
77
async with aiodns.DNSResolver() as resolver:
78
result = await resolver.query('example.com', 'A')
79
print(f"IP addresses: {[r.host for r in result]}")
80
# resolver.close() called automatically
81
82
asyncio.run(main())
83
```
84
85
## Capabilities
86
87
### DNS Query Operations
88
89
Perform DNS queries for various record types with full pycares integration.
90
91
```python { .api }
92
class DNSResolver:
93
def __init__(
94
self,
95
nameservers: Optional[Sequence[str]] = None,
96
loop: Optional[asyncio.AbstractEventLoop] = None,
97
**kwargs: Any
98
) -> None:
99
"""
100
Create a DNS resolver instance.
101
102
Parameters:
103
- nameservers: Optional list of DNS server IP addresses
104
- loop: Optional asyncio event loop (defaults to current loop)
105
- **kwargs: Additional options passed to pycares.Channel
106
"""
107
108
# Type-specific query method overloads
109
def query(self, host: str, qtype: Literal['A'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_a_result]]:
110
"""Query A records (IPv4 addresses)."""
111
112
def query(self, host: str, qtype: Literal['AAAA'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_aaaa_result]]:
113
"""Query AAAA records (IPv6 addresses)."""
114
115
def query(self, host: str, qtype: Literal['CAA'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_caa_result]]:
116
"""Query CAA records (Certificate Authority Authorization)."""
117
118
def query(self, host: str, qtype: Literal['CNAME'], qclass: Optional[str] = None) -> asyncio.Future[pycares.ares_query_cname_result]:
119
"""Query CNAME record (Canonical Name) - returns single result."""
120
121
def query(self, host: str, qtype: Literal['MX'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_mx_result]]:
122
"""Query MX records (Mail Exchange)."""
123
124
def query(self, host: str, qtype: Literal['NAPTR'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_naptr_result]]:
125
"""Query NAPTR records (Name Authority Pointer)."""
126
127
def query(self, host: str, qtype: Literal['NS'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_ns_result]]:
128
"""Query NS records (Name Server)."""
129
130
def query(self, host: str, qtype: Literal['PTR'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_ptr_result]]:
131
"""Query PTR records (Pointer)."""
132
133
def query(self, host: str, qtype: Literal['SOA'], qclass: Optional[str] = None) -> asyncio.Future[pycares.ares_query_soa_result]:
134
"""Query SOA record (Start of Authority) - returns single result."""
135
136
def query(self, host: str, qtype: Literal['SRV'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_srv_result]]:
137
"""Query SRV records (Service)."""
138
139
def query(self, host: str, qtype: Literal['TXT'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_txt_result]]:
140
"""Query TXT records (Text)."""
141
142
# Generic query method for runtime use
143
def query(self, host: str, qtype: str, qclass: Optional[str] = None) -> Union[asyncio.Future[list[Any]], asyncio.Future[Any]]:
144
"""
145
Perform DNS query for specified record type.
146
147
Parameters:
148
- host: Hostname or domain to query
149
- qtype: Query type ('A', 'AAAA', 'CNAME', 'MX', 'TXT', 'PTR', 'SOA', 'SRV', 'NS', 'CAA', 'NAPTR', 'ANY')
150
- qclass: Query class ('IN', 'CHAOS', 'HS', 'NONE', 'ANY'), defaults to 'IN'
151
152
Returns:
153
- asyncio.Future with query results (type varies by query type)
154
- Most queries return list[ResultType], but CNAME and SOA return single ResultType
155
156
Raises:
157
- ValueError: Invalid query type or class
158
- DNSError: DNS resolution errors
159
"""
160
```
161
162
### Hostname Resolution
163
164
Resolve hostnames to IP addresses with /etc/hosts file support.
165
166
```python { .api }
167
def gethostbyname(
168
self,
169
host: str,
170
family: socket.AddressFamily
171
) -> asyncio.Future[pycares.ares_host_result]:
172
"""
173
Resolve hostname to IP address, checking /etc/hosts first.
174
175
Parameters:
176
- host: Hostname to resolve
177
- family: Address family (socket.AF_INET or socket.AF_INET6)
178
179
Returns:
180
- asyncio.Future[pycares.ares_host_result] with host information
181
"""
182
183
def getaddrinfo(
184
self,
185
host: str,
186
family: socket.AddressFamily = socket.AF_UNSPEC,
187
port: Optional[int] = None,
188
proto: int = 0,
189
type: int = 0,
190
flags: int = 0
191
) -> asyncio.Future[pycares.ares_addrinfo_result]:
192
"""
193
Get address information for hostname (async equivalent of socket.getaddrinfo).
194
195
Parameters:
196
- host: Hostname to resolve
197
- family: Address family (socket.AF_INET, socket.AF_INET6, or socket.AF_UNSPEC)
198
- port: Port number (optional)
199
- proto: Protocol (0 for any)
200
- type: Socket type (0 for any)
201
- flags: Additional flags
202
203
Returns:
204
- asyncio.Future[pycares.ares_addrinfo_result] with address information
205
"""
206
```
207
208
### Reverse DNS Lookup
209
210
Perform reverse DNS lookups from IP addresses to hostnames.
211
212
```python { .api }
213
def gethostbyaddr(self, name: str) -> asyncio.Future[pycares.ares_host_result]:
214
"""
215
Perform reverse DNS lookup from IP address to hostname.
216
217
Parameters:
218
- name: IP address string to resolve
219
220
Returns:
221
- asyncio.Future[pycares.ares_host_result] with hostname information
222
"""
223
224
def getnameinfo(
225
self,
226
sockaddr: Union[tuple[str, int], tuple[str, int, int, int]],
227
flags: int = 0
228
) -> asyncio.Future[pycares.ares_nameinfo_result]:
229
"""
230
Reverse name resolution from socket address.
231
232
Parameters:
233
- sockaddr: Socket address tuple (IPv4: (host, port), IPv6: (host, port, flow, scope))
234
- flags: Additional flags
235
236
Returns:
237
- asyncio.Future[pycares.ares_nameinfo_result] with name information
238
"""
239
```
240
241
### Resource Management
242
243
Control resolver lifecycle and cancel operations.
244
245
```python { .api }
246
@property
247
def nameservers(self) -> Sequence[str]:
248
"""Get current DNS nameservers."""
249
250
@nameservers.setter
251
def nameservers(self, value: Iterable[Union[str, bytes]]) -> None:
252
"""Set DNS nameservers."""
253
254
def cancel(self) -> None:
255
"""Cancel all pending DNS queries. All futures will receive DNSError with ARES_ECANCELLED."""
256
257
async def close(self) -> None:
258
"""
259
Cleanly close the DNS resolver and release all resources.
260
Must be called when resolver is no longer needed.
261
"""
262
263
def __del__(self) -> None:
264
"""
265
Handle cleanup when the resolver is garbage collected.
266
Automatically calls _cleanup() to release resources if resolver was not properly closed.
267
Note: Explicit close() is preferred over relying on garbage collection.
268
"""
269
```
270
271
### Async Context Manager Support
272
273
Automatic resource cleanup using async context manager pattern.
274
275
```python { .api }
276
async def __aenter__(self) -> 'DNSResolver':
277
"""Enter async context manager."""
278
279
async def __aexit__(
280
self,
281
exc_type: Optional[type[BaseException]],
282
exc_val: Optional[BaseException],
283
exc_tb: Optional[TracebackType]
284
) -> None:
285
"""Exit async context manager, automatically calls close()."""
286
```
287
288
### Error Handling
289
290
DNS errors and status codes from the underlying pycares library.
291
292
```python { .api }
293
class DNSError(Exception):
294
"""Base class for all DNS errors."""
295
296
# Error constants (integers)
297
ARES_SUCCESS: int
298
ARES_ENOTFOUND: int # Domain name not found
299
ARES_EFORMERR: int # Format error
300
ARES_ESERVFAIL: int # Server failure
301
ARES_ENOTINITIALIZED: int # Not initialized
302
ARES_EBADNAME: int # Bad domain name
303
ARES_ETIMEOUT: int # Timeout
304
ARES_ECONNREFUSED: int # Connection refused
305
ARES_ENOMEM: int # Out of memory
306
ARES_ECANCELLED: int # Query cancelled
307
ARES_EBADQUERY: int # Bad query
308
ARES_EBADRESP: int # Bad response
309
ARES_ENODATA: int # No data
310
ARES_EBADFAMILY: int # Bad address family
311
ARES_EBADFLAGS: int # Bad flags
312
ARES_EBADHINTS: int # Bad hints
313
ARES_EBADSTR: int # Bad string
314
ARES_EREFUSED: int # Refused
315
ARES_ENOTIMP: int # Not implemented
316
ARES_ESERVICE: int # Service error
317
ARES_EFILE: int # File error
318
ARES_EOF: int # End of file
319
ARES_EDESTRUCTION: int # Destruction
320
ARES_ELOADIPHLPAPI: int # Load IP helper API error
321
ARES_EADDRGETNETWORKPARAMS: int # Address get network params error
322
```
323
324
## Types
325
326
Query result types vary by DNS record type:
327
328
```python { .api }
329
# A record results
330
pycares.ares_query_a_result:
331
host: str # IPv4 address
332
333
# AAAA record results
334
pycares.ares_query_aaaa_result:
335
host: str # IPv6 address
336
337
# CNAME record result
338
pycares.ares_query_cname_result:
339
cname: str # Canonical name
340
341
# MX record results
342
pycares.ares_query_mx_result:
343
host: str # Mail server hostname
344
priority: int # Priority value
345
346
# TXT record results
347
pycares.ares_query_txt_result:
348
text: bytes # Text record data
349
350
# PTR record results
351
pycares.ares_query_ptr_result:
352
name: str # Pointer target name
353
354
# SOA record result
355
pycares.ares_query_soa_result:
356
nsname: str # Primary nameserver
357
hostmaster: str # Responsible person email
358
serial: int # Serial number
359
refresh: int # Refresh interval
360
retry: int # Retry interval
361
expires: int # Expiry time
362
minttl: int # Minimum TTL
363
364
# SRV record results
365
pycares.ares_query_srv_result:
366
host: str # Target hostname
367
port: int # Port number
368
priority: int # Priority
369
weight: int # Weight
370
371
# NS record results
372
pycares.ares_query_ns_result:
373
host: str # Nameserver hostname
374
375
# CAA record results
376
pycares.ares_query_caa_result:
377
critical: int # Critical flag
378
property: bytes # Property name
379
value: bytes # Property value
380
381
# NAPTR record results
382
pycares.ares_query_naptr_result:
383
order: int # Order value
384
preference: int # Preference value
385
flags: bytes # Flags
386
service: bytes # Service
387
regexp: bytes # Regular expression
388
replacement: bytes # Replacement
389
390
# Host resolution results
391
pycares.ares_host_result:
392
name: str # Primary hostname
393
aliases: List[str] # Hostname aliases
394
addresses: List[str] # IP addresses
395
396
# Address info results
397
pycares.ares_addrinfo_result:
398
nodes: List[ares_addrinfo_node] # Address information nodes
399
400
# Address info node
401
pycares.ares_addrinfo_node:
402
family: int # Address family (socket.AF_INET, socket.AF_INET6)
403
socktype: int # Socket type
404
protocol: int # Protocol
405
addr: tuple # Address tuple (host, port) or (host, port, flow, scope)
406
canonname: str # Canonical name (optional)
407
408
# Name info results
409
pycares.ares_nameinfo_result:
410
node: str # Hostname
411
service: str # Service name
412
```
413
414
## Platform Notes
415
416
### Windows Compatibility
417
418
aiodns requires `asyncio.SelectorEventLoop` or `winloop` on Windows when using custom pycares builds without thread-safety. Official PyPI wheels (pycares 4.7.0+) include thread-safe c-ares and work with any event loop.
419
420
```python
421
# For non-thread-safe pycares builds on Windows:
422
import asyncio
423
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
424
```
425
426
### Thread Safety
427
428
aiodns automatically detects thread-safe pycares builds and optimizes accordingly:
429
- Thread-safe builds: Uses pycares event thread for better performance
430
- Non-thread-safe builds: Falls back to socket state callbacks with asyncio integration