pax_global_header00006660000000000000000000000064141475614500014521gustar00rootroot0000000000000052 comment=8aa39f1204e08c9d20302017ed175f3b453c1781 portend-3.1.0/000077500000000000000000000000001414756145000131755ustar00rootroot00000000000000portend-3.1.0/.coveragerc000066400000000000000000000001431414756145000153140ustar00rootroot00000000000000[run] omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* [report] show_missing = True portend-3.1.0/.editorconfig000066400000000000000000000003301414756145000156460ustar00rootroot00000000000000root = true [*] charset = utf-8 indent_style = tab indent_size = 4 insert_final_newline = true end_of_line = lf [*.py] indent_style = space max_line_length = 88 [*.{yml,yaml}] indent_style = space indent_size = 2 portend-3.1.0/.flake8000066400000000000000000000002101414756145000143410ustar00rootroot00000000000000[flake8] max-line-length = 88 # jaraco/skeleton#34 max-complexity = 10 extend-ignore = # Black creates whitespace before colon E203 portend-3.1.0/.github/000077500000000000000000000000001414756145000145355ustar00rootroot00000000000000portend-3.1.0/.github/dependabot.yml000066400000000000000000000002241414756145000173630ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" allow: - dependency-type: "all" portend-3.1.0/.github/workflows/000077500000000000000000000000001414756145000165725ustar00rootroot00000000000000portend-3.1.0/.github/workflows/main.yml000066400000000000000000000021001414756145000202320ustar00rootroot00000000000000name: tests on: [push, pull_request] jobs: test: strategy: matrix: python: - 3.7 - 3.9 - "3.10" platform: - ubuntu-latest - macos-latest - windows-latest runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install tox run: | python -m pip install tox - name: Run tests run: tox release: needs: test if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 with: python-version: "3.10" - name: Install tox run: | python -m pip install tox - name: Release run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} portend-3.1.0/.pre-commit-config.yaml000066400000000000000000000001211414756145000174500ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black portend-3.1.0/.readthedocs.yml000066400000000000000000000001171414756145000162620ustar00rootroot00000000000000version: 2 python: install: - path: . extra_requirements: - docs portend-3.1.0/CHANGES.rst000066400000000000000000000037511414756145000150050ustar00rootroot00000000000000v3.1.0 ====== Require Python 3.7 or later. v3.0.0 ====== Removed legacy aliases ``wait_for_occupied_port`` and ``wait_for_free_port``. v2.7.2 ====== Packaging refresh. v2.7.1 ====== #14: Fix host/port order. v2.7.0 ====== Refresh package. Require Python 3.6 or later. 2.6 === Package refresh. 2.5 === #10: Fix race condition in ``occupied`` and ``free``. 2.4 === #6: ``find_available_local_port`` now relies on ``socket.getaddrinfo`` to find a suitable address family. 2.3 === Package refresh. 2.2 === Merge with skeleton, including embedded license file. 2.1.2 ===== Fix README rendering. 2.1.1 ===== #5: Restored use of ``portend.client_host`` during ``assert_free`` check on Windows, fixing check when the bind address is ``*ADDR_ANY``. 2.1 === Use tempora.timing.Timer from tempora 1.8, replacing boilerplate code in occupied and free functions. #1: Removed ``portend._getaddrinfo`` and its usage in ``Checker.assert_free``. Dropped support for Python 2.6. 1.8 === Remove dependency on ``jaraco.compat`` and instead just copy and reference the ``total_seconds`` compatibility function for Python 2.6. 1.7.1 ===== * #2: Use tempora, replacing deprecated jaraco.timing. 1.7 === Expose the port check functionality as ``portend.Checker`` class. 1.6.1 ===== Correct failures on Python 2.6 where ``datetime.datetime.total_seconds`` and argparse are unavailable. 1.6 === Add support for Python 2.6 (to support CherryPy). 1.5 === Automatically deploy tagged versions via Travis-CI. 1.4 === Moved hosting to Github. 1.3 === Added ``find_available_local_port`` for identifying a local port available for binding. 1.2 === Only require ``pytest-runner`` when pytest is invoked. 1.1 === Renamed functions: - wait_for_occupied_port: occupied - wait_for_free_port: free The original names are kept as aliases for now. Added execution support for the portend module. Invoke with ``python -m portend``. 1.0 === Initial release based on utilities in CherryPy 3.5. portend-3.1.0/LICENSE000066400000000000000000000020321414756145000141770ustar00rootroot00000000000000Copyright Jason R. Coombs 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. portend-3.1.0/README.rst000066400000000000000000000042651414756145000146730ustar00rootroot00000000000000.. image:: https://img.shields.io/pypi/v/portend.svg :target: `PyPI link`_ .. image:: https://img.shields.io/pypi/pyversions/portend.svg :target: `PyPI link`_ .. _PyPI link: https://pypi.org/project/portend .. image:: https://github.com/jaraco/portend/workflows/tests/badge.svg :target: https://github.com/jaraco/portend/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. image:: https://readthedocs.org/projects/portend/badge/?version=latest :target: https://portend.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2021-informational :target: https://blog.jaraco.com/skeleton por·tend pôrˈtend/ verb be a sign or warning that (something, especially something momentous or calamitous) is likely to happen. Usage ===== Use portend to monitor TCP ports for bound or unbound states. For example, to wait for a port to be occupied, timing out after 3 seconds:: portend.occupied('www.google.com', 80, timeout=3) Or to wait for a port to be free, timing out after 5 seconds:: portend.free('::1', 80, timeout=5) The portend may also be executed directly. If the function succeeds, it returns nothing and exits with a status of 0. If it fails, it prints a message and exits with a status of 1. For example:: python -m portend localhost:31923 free (exits immediately) python -m portend -t 1 localhost:31923 occupied (one second passes) Port 31923 not bound on localhost. Portend also exposes a ``find_available_local_port`` for identifying a suitable port for binding locally:: port = portend.find_available_local_port() print(port, "is available for binding") Portend additionally exposes the lower-level port checking functionality in the ``Checker`` class, which currently exposes only one public method, ``assert_free``:: portend.Checker().assert_free('localhost', 31923) If assert_free is passed a host/port combination that is occupied by a bound listener (i.e. a TCP connection is established to that host/port), assert_free will raise a ``PortNotFree`` exception. portend-3.1.0/docs/000077500000000000000000000000001414756145000141255ustar00rootroot00000000000000portend-3.1.0/docs/conf.py000066400000000000000000000017631414756145000154330ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] master_doc = "index" link_files = { '../CHANGES.rst': dict( using=dict(GH='https://github.com'), replace=[ dict( pattern=r'(Issue #|\B#)(?P\d+)', url='{package_url}/issues/{issue}', ), dict( pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', ), dict( pattern=r'PEP[- ](?P\d+)', url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', ), ], ) } # Be strict about any broken references: nitpicky = True # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 extensions += ['sphinx.ext.intersphinx'] intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } portend-3.1.0/docs/history.rst000066400000000000000000000001211414756145000163520ustar00rootroot00000000000000:tocdepth: 2 .. _changes: History ******* .. include:: ../CHANGES (links).rst portend-3.1.0/docs/index.rst000066400000000000000000000004431414756145000157670ustar00rootroot00000000000000Welcome to |project| documentation! =================================== .. toctree:: :maxdepth: 1 history .. automodule:: portend :members: :undoc-members: :show-inheritance: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` portend-3.1.0/mypy.ini000066400000000000000000000000451414756145000146730ustar00rootroot00000000000000[mypy] ignore_missing_imports = True portend-3.1.0/portend.py000066400000000000000000000145301414756145000152250ustar00rootroot00000000000000""" A simple library for managing the availability of ports. """ import time import socket import argparse import sys import itertools import contextlib import platform from collections import abc import urllib.parse from tempora import timing def client_host(server_host): """ Return the host on which a client can connect to the given listener. >>> client_host('192.168.0.1') '192.168.0.1' >>> client_host('0.0.0.0') '127.0.0.1' >>> client_host('::') '::1' """ if server_host == '0.0.0.0': # 0.0.0.0 is INADDR_ANY, which should answer on localhost. return '127.0.0.1' if server_host in ('::', '::0', '::0.0.0.0'): # :: is IN6ADDR_ANY, which should answer on localhost. # ::0 and ::0.0.0.0 are non-canonical but common # ways to write IN6ADDR_ANY. return '::1' return server_host class Checker(object): def __init__(self, timeout=1.0): self.timeout = timeout def assert_free(self, host, port=None): """ Assert that the given addr is free in that all attempts to connect fail within the timeout or raise a PortNotFree exception. >>> free_port = find_available_local_port() >>> Checker().assert_free('localhost', free_port) >>> Checker().assert_free('127.0.0.1', free_port) >>> Checker().assert_free('::1', free_port) Also accepts an addr tuple >>> addr = '::1', free_port, 0, 0 >>> Checker().assert_free(addr) Host might refer to a server bind address like '::', which should use localhost to perform the check. >>> Checker().assert_free('::', free_port) """ if port is None and isinstance(host, abc.Sequence): host, port = host[:2] if platform.system() == 'Windows': host = client_host(host) # pragma: nocover info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM) list(itertools.starmap(self._connect, info)) def _connect(self, af, socktype, proto, canonname, sa): s = socket.socket(af, socktype, proto) # fail fast with a small timeout s.settimeout(self.timeout) with contextlib.closing(s): try: s.connect(sa) except socket.error: return # the connect succeeded, so the port isn't free host, port = sa[:2] tmpl = "Port {port} is in use on {host}." raise PortNotFree(tmpl.format(**locals())) class Timeout(IOError): pass class PortNotFree(IOError): pass def free(host, port, timeout=float('Inf')): """ Wait for the specified port to become free (dropping or rejecting requests). Return when the port is free or raise a Timeout if timeout has elapsed. Timeout may be specified in seconds or as a timedelta. If timeout is None or ∞, the routine will run indefinitely. >>> free('localhost', find_available_local_port()) >>> free(None, None) Traceback (most recent call last): ... ValueError: Host values of '' or None are not allowed. """ if not host: raise ValueError("Host values of '' or None are not allowed.") timer = timing.Timer(timeout) while True: try: # Expect a free port, so use a small timeout Checker(timeout=0.1).assert_free(host, port) return except PortNotFree: if timer.expired(): raise Timeout("Port {port} not free on {host}.".format(**locals())) # Politely wait. time.sleep(0.1) def occupied(host, port, timeout=float('Inf')): """ Wait for the specified port to become occupied (accepting requests). Return when the port is occupied or raise a Timeout if timeout has elapsed. Timeout may be specified in seconds or as a timedelta. If timeout is None or ∞, the routine will run indefinitely. >>> occupied('localhost', find_available_local_port(), .1) Traceback (most recent call last): ... Timeout: Port ... not bound on localhost. >>> occupied(None, None) Traceback (most recent call last): ... ValueError: Host values of '' or None are not allowed. """ if not host: raise ValueError("Host values of '' or None are not allowed.") timer = timing.Timer(timeout) while True: try: Checker(timeout=0.5).assert_free(host, port) if timer.expired(): raise Timeout("Port {port} not bound on {host}.".format(**locals())) # Politely wait time.sleep(0.1) except PortNotFree: # port is occupied return def find_available_local_port(): """ Find a free port on localhost. >>> 0 < find_available_local_port() < 65536 True """ infos = socket.getaddrinfo(None, 0, socket.AF_UNSPEC, socket.SOCK_STREAM) family, proto, _, _, addr = next(iter(infos)) sock = socket.socket(family, proto) sock.bind(addr) addr, port = sock.getsockname()[:2] sock.close() return port class HostPort(str): """ A simple representation of a host/port pair as a string >>> hp = HostPort('localhost:32768') >>> hp.host 'localhost' >>> hp.port 32768 >>> len(hp) 15 >>> hp = HostPort('[::1]:32768') >>> hp.host '::1' >>> hp.port 32768 """ @property def host(self): return urllib.parse.urlparse(f'//{self}').hostname @property def port(self): return urllib.parse.urlparse(f'//{self}').port @classmethod def from_addr(cls, addr): listen_host, port = addr[:2] plain_host = client_host(listen_host) host = f'[{plain_host}]' if ':' in plain_host else plain_host return cls(':'.join([host, str(port)])) def _main(args=None): parser = argparse.ArgumentParser() def global_lookup(key): return globals()[key] parser.add_argument('target', metavar='host:port', type=HostPort) parser.add_argument('func', metavar='state', type=global_lookup) parser.add_argument('-t', '--timeout', default=None, type=float) args = parser.parse_args(args) try: args.func(args.target.host, args.target.port, timeout=args.timeout) except Timeout as timeout: print(timeout, file=sys.stderr) raise SystemExit(1) __name__ == '__main__' and _main() portend-3.1.0/pyproject.toml000066400000000000000000000005461414756145000161160ustar00rootroot00000000000000[build-system] requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" [tool.black] skip-string-normalization = true [tool.setuptools_scm] [pytest.enabler.black] addopts = "--black" [pytest.enabler.mypy] addopts = "--mypy" [pytest.enabler.flake8] addopts = "--flake8" [pytest.enabler.cov] addopts = "--cov" portend-3.1.0/pytest.ini000066400000000000000000000005641414756145000152330ustar00rootroot00000000000000[pytest] norecursedirs=dist build .tox .eggs addopts=--doctest-modules doctest_optionflags=ALLOW_UNICODE ELLIPSIS IGNORE_EXCEPTION_DETAIL filterwarnings= # Suppress deprecation warning in flake8 ignore:SelectableGroups dict interface is deprecated::flake8 # Suppress deprecation warning in pypa/packaging#433 ignore:The distutils package is deprecated::packaging.tags portend-3.1.0/setup.cfg000066400000000000000000000020561414756145000150210ustar00rootroot00000000000000[metadata] name = portend author = Jason R. Coombs author_email = jaraco@jaraco.com description = TCP port monitoring and discovery long_description = file:README.rst url = https://github.com/jaraco/portend classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only [options] packages = find_namespace: py_modules = portend include_package_data = true python_requires = >=3.7 install_requires = tempora>=1.8 [options.packages.find] exclude = build* dist* docs* tests* [options.extras_require] testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 pytest-flake8 pytest-black >= 0.3.7; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-cov pytest-mypy; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 1.0.1 # local docs = # upstream sphinx jaraco.packaging >= 8.2 rst.linker >= 1.9 # local [options.entry_points] portend-3.1.0/setup.py000066400000000000000000000001341414756145000147050ustar00rootroot00000000000000#!/usr/bin/env python import setuptools if __name__ == "__main__": setuptools.setup() portend-3.1.0/test_portend.py000066400000000000000000000044331414756145000162650ustar00rootroot00000000000000import socket import contextlib import pytest from tempora import timing import portend def socket_infos(): """ Generate addr infos for connections to localhost """ host = None # all available interfaces port = portend.find_available_local_port() family = socket.AF_UNSPEC socktype = socket.SOCK_STREAM proto = 0 flags = socket.AI_PASSIVE return socket.getaddrinfo(host, port, family, socktype, proto, flags) def id_for_info(info): (af,) = info[:1] return str(af) def build_addr_infos(): params = list(socket_infos()) ids = list(map(id_for_info, params)) return locals() @pytest.fixture(**build_addr_infos()) def listening_addr(request): af, socktype, proto, canonname, sa = request.param sock = socket.socket(af, socktype, proto) sock.bind(sa) sock.listen(5) with contextlib.closing(sock): yield sa @pytest.fixture(**build_addr_infos()) def nonlistening_addr(request): af, socktype, proto, canonname, sa = request.param return sa @pytest.fixture def immediate_timeout(monkeypatch): monkeypatch.setattr(timing.Timer, 'expired', lambda: True) class TestChecker: def test_check_port_listening(self, listening_addr): with pytest.raises(portend.PortNotFree): portend.Checker().assert_free(listening_addr) def test_check_port_nonlistening(self, nonlistening_addr): portend.Checker().assert_free(nonlistening_addr) def test_free_with_immediate_timeout(self, nonlistening_addr, immediate_timeout): host, port = nonlistening_addr[:2] portend.free(host, port, timeout=1.0) def test_free_with_timeout(self, listening_addr): host, port = listening_addr[:2] with pytest.raises(portend.Timeout): portend.free(*listening_addr[:2], timeout=0.3) def test_occupied_with_immediate_timeout(self, listening_addr, immediate_timeout): host, port = listening_addr[:2] portend.occupied(host, port, timeout=1.0) def test_main(listening_addr): target = portend.HostPort.from_addr(listening_addr) portend._main([target, 'occupied']) def test_main_timeout(listening_addr): target = portend.HostPort.from_addr(listening_addr) with pytest.raises(SystemExit): portend._main([target, 'free', '-t', '0.1']) portend-3.1.0/tox.ini000066400000000000000000000013341414756145000145110ustar00rootroot00000000000000[tox] envlist = python minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = commands = pytest {posargs} usedevelop = True extras = testing [testenv:docs] extras = docs testing changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html [testenv:release] skip_install = True deps = build twine>=3 jaraco.develop>=7.1 passenv = TWINE_PASSWORD GITHUB_TOKEN setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" python -m build python -m twine upload dist/* python -m jaraco.develop.create-github-release