pax_global_header00006660000000000000000000000064140474346540014525gustar00rootroot0000000000000052 comment=cdb33385f46be1e18bc525ccb153c8abc8ac92d4 aiodns-aiodns-3.0.0/000077500000000000000000000000001404743465400142555ustar00rootroot00000000000000aiodns-aiodns-3.0.0/.github/000077500000000000000000000000001404743465400156155ustar00rootroot00000000000000aiodns-aiodns-3.0.0/.github/workflows/000077500000000000000000000000001404743465400176525ustar00rootroot00000000000000aiodns-aiodns-3.0.0/.github/workflows/ci.yml000066400000000000000000000013011404743465400207630ustar00rootroot00000000000000name: CI on: [pull_request] jobs: build: name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-20.04, windows-2019, macos-10.15] python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy-3.6', 'pypy-3.7' ] steps: - uses: actions/checkout@v2 with: submodules: true - name: Setup Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Build package run: python setup.py install - name: Run tests run: python tests.py - name: Run mypy tests run: python mypy_run.py aiodns-aiodns-3.0.0/.github/workflows/release-wheels.yml000066400000000000000000000024551404743465400233100ustar00rootroot00000000000000name: Release Wheels on: release: types: - published jobs: build_wheels: name: Build wheels runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Install Python with: python-version: '3.8' - run: pip install wheel - name: Build wheels run: python setup.py bdist_wheel - uses: actions/upload-artifact@v2 with: path: dist/*.whl build_sdist: name: Build source distribution runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Install Python with: python-version: '3.8' - name: Build sdist run: python setup.py sdist - uses: actions/upload-artifact@v2 with: path: dist/*.tar.gz upload_pypi: needs: [build_wheels, build_sdist] runs-on: ubuntu-20.04 # upload to PyPI when a GitHub Release is created if: github.event_name == 'release' && github.event.action == 'published' steps: - uses: actions/download-artifact@v2 with: name: artifact path: dist - uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_password }} aiodns-aiodns-3.0.0/.gitignore000066400000000000000000000002671404743465400162520ustar00rootroot00000000000000# 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/ aiodns-aiodns-3.0.0/ChangeLog000066400000000000000000000034461404743465400160360ustar00rootroot000000000000003.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 aiodns-aiodns-3.0.0/LICENSE000066400000000000000000000020561404743465400152650ustar00rootroot00000000000000Copyright (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. aiodns-aiodns-3.0.0/MANIFEST.in000066400000000000000000000001751404743465400160160ustar00rootroot00000000000000include README.rst LICENSE ChangeLog include setup.py tests.py recursive-exclude * __pycache__ recursive-exclude * *.py[co] aiodns-aiodns-3.0.0/README.rst000066400000000000000000000051221404743465400157440ustar00rootroot00000000000000=============================== 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 `_. 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 `_ 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 `_ 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é 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 :-) aiodns-aiodns-3.0.0/aiodns/000077500000000000000000000000001404743465400155325ustar00rootroot00000000000000aiodns-aiodns-3.0.0/aiodns/__init__.py000066400000000000000000000117311404743465400176460ustar00rootroot00000000000000 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 aiodns-aiodns-3.0.0/aiodns/error.py000066400000000000000000000002061404743465400172330ustar00rootroot00000000000000 import pycares for code, name in pycares.errno.errorcode.items(): globals()[name] = code class DNSError(Exception): pass aiodns-aiodns-3.0.0/mypy.ini000066400000000000000000000000451404743465400157530ustar00rootroot00000000000000[mypy] ignore_missing_imports = True aiodns-aiodns-3.0.0/mypy_run.py000066400000000000000000000006631404743465400165160ustar00rootroot00000000000000#!/usr/bin/env python from subprocess import run import sys # Check if we're not cpython - Exit cleanly if so if sys.implementation.name != "cpython": sys.exit(0) # We only want to install if we're cpython too install_success = run("pip install mypy", shell=True).returncode if install_success: print("mypy install failed", file=sys.stderr) sys.exit(install_success) sys.exit(run("mypy aiodns", shell=True).returncode) aiodns-aiodns-3.0.0/setup.cfg000066400000000000000000000000161404743465400160730ustar00rootroot00000000000000[bdist_wheel] aiodns-aiodns-3.0.0/setup.py000066400000000000000000000026161404743465400157740ustar00rootroot00000000000000# -*- coding: utf-8 -*- import codecs import re import sys from setuptools import setup def get_version(): return re.search(r"""__version__\s+=\s+(?P['"])(?P.+?)(?P=quote)""", open('aiodns/__init__.py').read()).group('version') setup(name = "aiodns", version = get_version(), author = "Saúl Ibarra Corretgé", author_email = "s@saghul.net", url = "https://github.com/saghul/aiodns", description = "Simple DNS resolver for asyncio", license = "MIT", long_description = codecs.open("README.rst", encoding="utf-8").read(), long_description_content_type = "text/x-rst", install_requires = ['pycares>=4.0.0'], packages = ['aiodns'], platforms = ["POSIX", "Microsoft Windows"], classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9" ] ) aiodns-aiodns-3.0.0/tests.py000077500000000000000000000124641404743465400160030ustar00rootroot00000000000000#!/usr/bin/env python import asyncio import ipaddress import unittest import socket import sys import aiodns class DNSTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() self.addCleanup(self.loop.close) self.resolver = aiodns.DNSResolver(loop=self.loop, timeout=5.0) self.resolver.nameservers = ['8.8.8.8'] def tearDown(self): self.resolver = None def test_query_a(self): f = self.resolver.query('google.com', 'A') result = self.loop.run_until_complete(f) def test_query_async_await(self): async def f(): return await self.resolver.query('google.com', 'A') result = self.loop.run_until_complete(f()) self.assertTrue(result) def test_query_a_bad(self): f = self.resolver.query('hgf8g2od29hdohid.com', 'A') try: self.loop.run_until_complete(f) except aiodns.error.DNSError as e: self.assertEqual(e.args[0], aiodns.error.ARES_ENOTFOUND) def test_query_aaaa(self): f = self.resolver.query('ipv6.google.com', 'AAAA') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_cname(self): f = self.resolver.query('www.amazon.com', 'CNAME') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_mx(self): f = self.resolver.query('google.com', 'MX') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_ns(self): f = self.resolver.query('google.com', 'NS') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_txt(self): f = self.resolver.query('google.com', 'TXT') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_soa(self): f = self.resolver.query('google.com', 'SOA') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_srv(self): f = self.resolver.query('_xmpp-server._tcp.jabber.org', 'SRV') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_naptr(self): f = self.resolver.query('sip2sip.info', 'NAPTR') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_ptr(self): ip = '8.8.8.8' f = self.resolver.query(ipaddress.ip_address(ip).reverse_pointer, 'PTR') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_bad_type(self): self.assertRaises(ValueError, self.resolver.query, 'google.com', 'XXX') def test_query_txt_chaos(self): self.resolver = aiodns.DNSResolver(loop=self.loop) self.resolver.nameservers = ['1.1.1.1'] f = self.resolver.query('id.server', 'TXT', 'CHAOS') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_query_bad_class(self): self.assertRaises(ValueError, self.resolver.query, 'google.com', 'A', "INVALIDCLASS") def test_query_timeout(self): self.resolver = aiodns.DNSResolver(timeout=0.1, loop=self.loop) self.resolver.nameservers = ['1.2.3.4'] f = self.resolver.query('google.com', 'A') try: self.loop.run_until_complete(f) except aiodns.error.DNSError as e: self.assertEqual(e.args[0], aiodns.error.ARES_ETIMEOUT) def test_query_cancel(self): f = self.resolver.query('google.com', 'A') self.resolver.cancel() try: self.loop.run_until_complete(f) except aiodns.error.DNSError as e: self.assertEqual(e.args[0], aiodns.error.ARES_ECANCELLED) def test_future_cancel(self): f = self.resolver.query('google.com', 'A') f.cancel() async def coro(): await asyncio.sleep(0.1, loop=self.loop) await f try: self.loop.run_until_complete(coro()) except asyncio.CancelledError as e: self.assertTrue(e) def test_query_twice(self): async def coro(self, host, qtype, n=2): for i in range(n): result = await self.resolver.query(host, qtype) self.assertTrue(result) self.loop.run_until_complete(coro(self, 'gmail.com', 'MX')) def test_gethostbyname(self): f = self.resolver.gethostbyname('google.com', socket.AF_INET) result = self.loop.run_until_complete(f) self.assertTrue(result) def test_gethostbyaddr(self): f = self.resolver.gethostbyaddr('127.0.0.1') result = self.loop.run_until_complete(f) self.assertTrue(result) def test_gethostbyname_ipv6(self): f = self.resolver.gethostbyname('ipv6.google.com', socket.AF_INET6) result = self.loop.run_until_complete(f) self.assertTrue(result) def test_gethostbyname_bad_family(self): f = self.resolver.gethostbyname('ipv6.google.com', -1) with self.assertRaises(aiodns.error.DNSError): self.loop.run_until_complete(f) def test_query_bad_chars(self): f = self.resolver.query('xn--cardeosapeluqueros-r0b.com', 'MX') result = self.loop.run_until_complete(f) self.assertTrue(result) if __name__ == '__main__': unittest.main(verbosity=2)