Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update aiodns to 3.0.0 #1

Merged
merged 1 commit into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# python specific
*.pyc
*.so
*.pyd
build/*
dist/*
MANIFEST
__pycache__/
*.egg-info/

# generic files to ignore
*~
*.lock
*.DS_Store
*.swp
*.out

.tox/
deps/
docs/_build/
.mypy_cache/
93 changes: 93 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
3.0.0
=====
- Release wheels and source to PyPI with GH actions
- Try to make tests more resilient
- Don't build universal wheels
- Migrate CI to GH Actions
- Fix TXT CHAOS test
- Add support for CAA queries
- Support Python >= 3.6
- Bump pycares dependency
- Drop tasks.py
- Allow specifying dnsclass for queries
- Set URL to https
- Add license args in setup.py
- Converted Type Annotations to Py3 syntax Closes
- Only run mypy on cpython versions
- Also fix all type errors with latest mypy - pycares seems to have no typing / stubs so lets ignore it via `mypy.ini`
- setup: typing exists since Python 3.5
- Fix type annotation of gethostbyname()
- Updated README

2.0.0
=====
(changes since version 1.x)

- Drop support for Python < 3.5
- Add support for ANY queries
- Raise pycares dependency

2.0.0b2
=======
- Raise pycares dependency

2.0.0b1
=======
- Fix using typing on Python 3.7

2.0.0b0
=======
- Drop support for Python < 3.5
- Add support for ANY queries
- Raise pycares dependency

1.2.0
=====
- Add support for Python 3.7
- Fix CNAME test
- Add examples with `async` and `await`
- Fix Python version check
- Add gethostbyaddr

1.1.1
=====
- Use per-version requires for wheels

1.1.0
=====
- Add DNSResolver.gethostbyname()
- Build universal wheels

1.0.1
=====
- Fix including tests and ChangeLog in source distributions

1.0.0
=====
- Use pycares >= 1.0.0
- Fix tests

0.3.2
=====
- setup: Fix decoding in non-UTF-8 environments

0.3.1
=====
- Adapt to Trollius package rename
- Fixed stopping watching file descriptor

0.3.0
=====
- Add DNSResolver.cancel method
- Handle case when the Future returned by query() is cancelled

0.2.0
=====
- Add support for Trollius
- Don't make query() a coroutine, just return the future
- Raise ValueError if specified query type is invalid

0.1.0
=====
- Initial release

20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (C) 2014 by Saúl Ibarra Corretgé

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

5 changes: 5 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include README.rst LICENSE ChangeLog
include setup.py tests.py
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

1 change: 0 additions & 1 deletion README.md

This file was deleted.

87 changes: 87 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
===============================
Simple DNS resolver for asyncio
===============================

.. image:: https://badge.fury.io/py/aiodns.png
:target: https://pypi.org/project/aiodns/

.. image:: https://github.com/saghul/aiodns/workflows/CI/badge.svg
:target: https://github.com/saghul/aiodns/actions

aiodns provides a simple way for doing asynchronous DNS resolutions using `pycares <https://github.com/saghul/pycares>`_.


Example
=======

.. code:: python

import asyncio
import aiodns

loop = asyncio.get_event_loop()
resolver = aiodns.DNSResolver(loop=loop)

async def query(name, query_type):
return await resolver.query(name, query_type)

coro = query('google.com', 'A')
result = loop.run_until_complete(coro)


The following query types are supported: A, AAAA, ANY, CAA, CNAME, MX, NAPTR, NS, PTR, SOA, SRV, TXT.


API
===

The API is pretty simple, three functions are provided in the ``DNSResolver`` class:

* ``query(host, type)``: Do a DNS resolution of the given type for the given hostname. It returns an
instance of ``asyncio.Future``. The actual result of the DNS query is taken directly from pycares.
As of version 1.0.0 of aiodns (and pycares, for that matter) results are always namedtuple-like
objects with different attributes. Please check the `documentation
<http://pycares.readthedocs.org/en/latest/channel.html#pycares.Channel.query>`_
for the result fields.
* ``gethostbyname(host, socket_family)``: Do a DNS resolution for the given
hostname and the desired type of address family (i.e. ``socket.AF_INET``).
While ``query()`` always performs a request to a DNS server,
``gethostbyname()`` first looks into ``/etc/hosts`` and thus can resolve
local hostnames (such as ``localhost``). Please check `the documentation
<http://pycares.readthedocs.io/en/latest/channel.html#pycares.Channel.gethostbyname>`_
for the result fields. The actual result of the call is a ``asyncio.Future``.
* ``gethostbyaddr(name)``: Make a reverse lookup for an address.
* ``cancel()``: Cancel all pending DNS queries. All futures will get ``DNSError`` exception set, with
``ARES_ECANCELLED`` errno.


Running the test suite
======================

To run the test suite: ``python tests.py``


Author
======

Saúl Ibarra Corretgé <[email protected]>


License
=======

aiodns uses the MIT license, check LICENSE file.


Python versions
===============

Python >= 3.6 are supported.


Contributing
============

If you'd like to contribute, fork the project, make a patch and send a pull
request. Have a look at the surrounding code and please, make yours look
alike :-)
146 changes: 146 additions & 0 deletions aiodns/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@

import asyncio
import functools
import pycares
import socket

from typing import (
Any,
List,
Optional,
Set
)

from . import error


__version__ = '3.0.0'

__all__ = ('DNSResolver', 'error')


READ = 1
WRITE = 2

query_type_map = {'A' : pycares.QUERY_TYPE_A,
'AAAA' : pycares.QUERY_TYPE_AAAA,
'ANY' : pycares.QUERY_TYPE_ANY,
'CAA' : pycares.QUERY_TYPE_CAA,
'CNAME' : pycares.QUERY_TYPE_CNAME,
'MX' : pycares.QUERY_TYPE_MX,
'NAPTR' : pycares.QUERY_TYPE_NAPTR,
'NS' : pycares.QUERY_TYPE_NS,
'PTR' : pycares.QUERY_TYPE_PTR,
'SOA' : pycares.QUERY_TYPE_SOA,
'SRV' : pycares.QUERY_TYPE_SRV,
'TXT' : pycares.QUERY_TYPE_TXT
}

query_class_map = {'IN' : pycares.QUERY_CLASS_IN,
'CHAOS' : pycares.QUERY_CLASS_CHAOS,
'HS' : pycares.QUERY_CLASS_HS,
'NONE' : pycares.QUERY_CLASS_NONE,
'ANY' : pycares.QUERY_CLASS_ANY
}

class DNSResolver:
def __init__(self, nameservers: Optional[List[str]] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
**kwargs: Any) -> None:
self.loop = loop or asyncio.get_event_loop()
assert self.loop is not None
kwargs.pop('sock_state_cb', None)
self._channel = pycares.Channel(sock_state_cb=self._sock_state_cb, **kwargs)
if nameservers:
self.nameservers = nameservers
self._read_fds = set() # type: Set[int]
self._write_fds = set() # type: Set[int]
self._timer = None # type: Optional[asyncio.TimerHandle]

@property
def nameservers(self) -> pycares.Channel:
return self._channel.servers

@nameservers.setter
def nameservers(self, value: List[str]) -> None:
self._channel.servers = value

@staticmethod
def _callback(fut: asyncio.Future, result: Any, errorno: int) -> None:
if fut.cancelled():
return
if errorno is not None:
fut.set_exception(error.DNSError(errorno, pycares.errno.strerror(errorno)))
else:
fut.set_result(result)

def query(self, host: str, qtype: str, qclass: str=None) -> asyncio.Future:
try:
qtype = query_type_map[qtype]
except KeyError:
raise ValueError('invalid query type: {}'.format(qtype))
if qclass is not None:
try:
qclass = query_class_map[qclass]
except KeyError:
raise ValueError('invalid query class: {}'.format(qclass))

fut = asyncio.Future(loop=self.loop) # type: asyncio.Future
cb = functools.partial(self._callback, fut)
self._channel.query(host, qtype, cb, query_class=qclass)
return fut

def gethostbyname(self, host: str, family: socket.AddressFamily) -> asyncio.Future:
fut = asyncio.Future(loop=self.loop) # type: asyncio.Future
cb = functools.partial(self._callback, fut)
self._channel.gethostbyname(host, family, cb)
return fut

def gethostbyaddr(self, name: str) -> asyncio.Future:
fut = asyncio.Future(loop=self.loop) # type: asyncio.Future
cb = functools.partial(self._callback, fut)
self._channel.gethostbyaddr(name, cb)
return fut

def cancel(self) -> None:
self._channel.cancel()

def _sock_state_cb(self, fd: int, readable: bool, writable: bool) -> None:
if readable or writable:
if readable:
self.loop.add_reader(fd, self._handle_event, fd, READ)
self._read_fds.add(fd)
if writable:
self.loop.add_writer(fd, self._handle_event, fd, WRITE)
self._write_fds.add(fd)
if self._timer is None:
self._timer = self.loop.call_later(1.0, self._timer_cb)
else:
# socket is now closed
if fd in self._read_fds:
self._read_fds.discard(fd)
self.loop.remove_reader(fd)

if fd in self._write_fds:
self._write_fds.discard(fd)
self.loop.remove_writer(fd)

if not self._read_fds and not self._write_fds and self._timer is not None:
self._timer.cancel()
self._timer = None

def _handle_event(self, fd: int, event: Any) -> None:
read_fd = pycares.ARES_SOCKET_BAD
write_fd = pycares.ARES_SOCKET_BAD
if event == READ:
read_fd = fd
elif event == WRITE:
write_fd = fd
self._channel.process_fd(read_fd, write_fd)

def _timer_cb(self) -> None:
if self._read_fds or self._write_fds:
self._channel.process_fd(pycares.ARES_SOCKET_BAD, pycares.ARES_SOCKET_BAD)
self._timer = self.loop.call_later(1.0, self._timer_cb)
else:
self._timer = None
10 changes: 10 additions & 0 deletions aiodns/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

import pycares

for code, name in pycares.errno.errorcode.items():
globals()[name] = code


class DNSError(Exception):
pass

Loading
Loading