pax_global_header00006660000000000000000000000064135564214270014523gustar00rootroot0000000000000052 comment=9ce3b8ec20ae42c7c8da87f10d17c9f4de92f82c portend-2.6/000077500000000000000000000000001355642142700130455ustar00rootroot00000000000000portend-2.6/.coveragerc000066400000000000000000000000621355642142700151640ustar00rootroot00000000000000[run] omit = .tox/* [report] show_missing = True portend-2.6/.flake8000066400000000000000000000003661355642142700142250ustar00rootroot00000000000000[flake8] max-line-length = 88 ignore = # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 W503 # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 W504 # Black creates whitespace before colon E203 portend-2.6/.pre-commit-config.yaml000066400000000000000000000001211355642142700173200ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 19.3b0 hooks: - id: black portend-2.6/.readthedocs.yml000066400000000000000000000001121355642142700161250ustar00rootroot00000000000000python: version: 3 extra_requirements: - docs pip_install: true portend-2.6/.travis.yml000066400000000000000000000007301355642142700151560ustar00rootroot00000000000000dist: xenial language: python python: - 2.7 - 3.6 - &latest_py3 3.8 jobs: fast_finish: true include: - stage: deploy if: tag IS present python: *latest_py3 before_script: skip script: tox -e release cache: pip install: - pip install tox tox-venv before_script: # Disable IPv6. Ref travis-ci/travis-ci#8361 - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi script: tox portend-2.6/CHANGES.rst000066400000000000000000000033161355642142700146520ustar00rootroot000000000000002.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-2.6/LICENSE000066400000000000000000000020321355642142700140470ustar00rootroot00000000000000Copyright 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-2.6/README.rst000066400000000000000000000041521355642142700145360ustar00rootroot00000000000000.. image:: https://img.shields.io/pypi/v/portend.svg :target: https://pypi.org/project/portend .. image:: https://img.shields.io/pypi/pyversions/portend.svg .. image:: https://img.shields.io/travis/jaraco/portend/master.svg :target: https://travis-ci.org/jaraco/portend .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. image:: https://img.shields.io/appveyor/ci/jaraco/portend/master.svg :target: https://ci.appveyor.com/project/jaraco/portend/branch/master .. image:: https://readthedocs.org/projects/portend/badge/?version=latest :target: https://portend.readthedocs.io/en/latest/?badge=latest 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-2.6/appveyor.yml000066400000000000000000000007241355642142700154400ustar00rootroot00000000000000environment: APPVEYOR: true matrix: - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python27-x64" install: # symlink python from a directory with a space - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%" - "SET PYTHON=\"C:\\Program Files\\Python\"" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" build: off cache: - '%LOCALAPPDATA%\pip\Cache' test_script: - "python -m pip install -U tox tox-venv virtualenv" - "tox" version: '{build}' portend-2.6/docs/000077500000000000000000000000001355642142700137755ustar00rootroot00000000000000portend-2.6/docs/conf.py000066400000000000000000000013631355642142700152770ustar00rootroot00000000000000#!/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}/', ), ], ) } portend-2.6/docs/history.rst000066400000000000000000000001211355642142700162220ustar00rootroot00000000000000:tocdepth: 2 .. _changes: History ******* .. include:: ../CHANGES (links).rst portend-2.6/docs/index.rst000066400000000000000000000004371355642142700156420ustar00rootroot00000000000000Welcome to portend documentation! ================================= .. toctree:: :maxdepth: 1 history .. automodule:: portend :members: :undoc-members: :show-inheritance: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` portend-2.6/portend.py000066400000000000000000000134211355642142700150730ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ A simple library for managing the availability of ports. """ from __future__ import print_function, division import time import socket import argparse import sys import itertools import contextlib import platform try: from collections import abc except ImportError: import collections as abc from tempora import timing def client_host(server_host): """Return the host on which a client can connect to the given listener.""" 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) 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 port, host = 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()) """ 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) wait_for_free_port = free 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. """ 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 wait_for_occupied_port = occupied 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 """ @property def host(self): host, sep, port = self.partition(':') return host @property def port(self): host, sep, port = self.partition(':') return int(port) def _main(): 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() try: args.func(args.target.host, args.target.port, timeout=args.timeout) except Timeout as timeout: print(timeout, file=sys.stderr) raise SystemExit(1) if __name__ == '__main__': _main() portend-2.6/pyproject.toml000066400000000000000000000002471355642142700157640ustar00rootroot00000000000000[build-system] requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"] build-backend = "setuptools.build_meta" [tool.black] skip-string-normalization = true portend-2.6/pytest.ini000066400000000000000000000007561355642142700151060ustar00rootroot00000000000000[pytest] norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 --black --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS IGNORE_EXCEPTION_DETAIL filterwarnings= ignore:Possible nested set::pycodestyle:113 ignore:Using or importing the ABCs::flake8:410 # workaround for https://sourceforge.net/p/docutils/bugs/348/ ignore:'U' mode is deprecated::docutils.io # workaround for https://gitlab.com/pycqa/flake8/issues/275 ignore:You passed a bytestring as `filenames`.::flake8 portend-2.6/setup.cfg000066400000000000000000000015611355642142700146710ustar00rootroot00000000000000[bdist_wheel] universal = 1 [metadata] license_file = LICENSE 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 :: 2.7 Programming Language :: Python :: 3 [options] py_modules = portend include_package_data = true python_requires = >=2.7 install_requires = tempora>=1.8 setup_requires = setuptools_scm >= 1.15.0 [options.extras_require] testing = # upstream pytest >= 3.5, !=3.7.3 pytest-checkdocs pytest-flake8 pytest-black-multipy pytest-cov # local docs = # upstream sphinx jaraco.packaging >= 3.2 rst.linker >= 1.9 # local [options.entry_points] portend-2.6/setup.py000066400000000000000000000001601355642142700145540ustar00rootroot00000000000000#!/usr/bin/env python import setuptools if __name__ == "__main__": setuptools.setup(use_scm_version=True) portend-2.6/skeleton.md000066400000000000000000000177261355642142700152300ustar00rootroot00000000000000# Overview This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution. ## An SCM Managed Approach While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a git repo capturing the evolution and culmination of these best practices. It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter. The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects. Another advantage to using an SCM-managed approach is that tools like GitHub recognize that a change in the skeleton is the _same change_ across all projects that merge with that skeleton. Without the ancestry, with a traditional copy/paste approach, a [commit like this](https://github.com/jaraco/skeleton/commit/12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5) would produce notifications in the upstream project issue for each and every application, but because it's centralized, GitHub provides just the one notification when the change is added to the skeleton. # Usage ## new projects To use skeleton for a new project, simply pull the skeleton into a new project: ``` $ git init my-new-project $ cd my-new-project $ git pull gh://jaraco/skeleton ``` Now customize the project to suit your individual project needs. ## existing projects If you have an existing project, you can still incorporate the skeleton by merging it into the codebase. ``` $ git merge skeleton --allow-unrelated-histories ``` The `--allow-unrelated-histories` is necessary because the history from the skeleton was previously unrelated to the existing codebase. Resolve any merge conflicts and commit to the master, and now the project is based on the shared skeleton. ## Updating Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar git operations. Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. # Features The features/techniques employed by the skeleton include: - PEP 517/518 based build relying on setuptools as the build tool - setuptools declarative configuration using setup.cfg - tox for running tests - A README.rst as reStructuredText with some popular badges, but with readthedocs and appveyor badges commented out - A CHANGES.rst file intended for publishing release notes about the project - Use of [black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) ## Packaging Conventions A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on setuptools (a minimum version compatible with setup.cfg declarative config). The setup.cfg file implements the following features: - Assumes universal wheel for release - Advertises the project's LICENSE file (MIT by default) - Reads the README.rst file into the long description - Some common Trove classifiers - Includes all packages discovered in the repo - Data files in the package are also included (not just Python files) - Declares the required Python versions - Declares install requirements (empty by default) - Declares setup requirements for legacy environments - Supplies two 'extras': - testing: requirements for running tests - docs: requirements for building docs - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project); these markers help avoid merge conflicts - Placeholder for defining entry points Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things: - derive the project version from SCM tags - ensure that all files committed to the repo are automatically included in releases ## Running Tests The skeleton assumes the developer has [tox](https://pypi.org/project/tox) installed. The developer is expected to run `tox` to run tests on the current Python version using [pytest](https://pypi.org/project/pytest). Other environments (invoked with `tox -e {name}`) supplied include: - a `build-docs` environment to build the documentation - a `release` environment to publish the package to PyPI A pytest.ini is included to define common options around running tests. In particular: - rely on default test discovery in the current directory - avoid recursing into common directories not containing tests - run doctests on modules and invoke flake8 tests - in doctests, allow unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option. - filters out known warnings caused by libraries/functionality included by the skeleton Relies a .flake8 file to correct some default behaviors: - disable mutually incompatible rules W503 and W504 - support for black format ## Continuous Integration The project is pre-configured to run tests in [Travis-CI](https://travis-ci.org) (.travis.yml). Any new project must be enabled either through their web site or with the `travis enable` command. Features include: - test against Python 2 and 3 - run on Ubuntu Xenial - correct for broken IPv6 Also provided is a minimal template for running under Appveyor (Windows). ### Continuous Deployments In addition to running tests, an additional deploy stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with Travis as the TWINE_PASSWORD environment variable. After the Travis project is created, configure the token through the web UI or with a command like the following (bash syntax): ``` TWINE_PASSWORD={token} travis env copy TWINE_PASSWORD ``` ## Building Documentation Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e build-docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. In addition to building the sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. ## Cutting releases By default, tagged commits are released through the continuous integration deploy stage. Releases may also be cut manually by invoking the tox environment `release` with the PyPI token set as the TWINE_PASSWORD: ``` TWINE_PASSWORD={token} tox -e release ``` portend-2.6/test_portend.py000066400000000000000000000034271355642142700161370ustar00rootroot00000000000000import 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_occupied_with_immediate_timeout(self, listening_addr, immediate_timeout): host, port = listening_addr[:2] portend.occupied(host, port, timeout=1.0) portend-2.6/tox.ini000066400000000000000000000014021355642142700143550ustar00rootroot00000000000000[tox] envlist = python minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true # Ensure that a late version of pip is used even on tox-venv. requires = tox-pip-version>=0.0.6 tox-venv [testenv] deps = setuptools>=31.0.1 pip_version = pip commands = pytest {posargs} usedevelop = True extras = testing [testenv:build-docs] extras = docs testing changedir = docs commands = python -m sphinx . {toxinidir}/build/html [testenv:release] skip_install = True deps = pep517>=0.5 twine>=1.13 path.py passenv = TWINE_PASSWORD setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import path; path.Path('dist').rmtree_p()" python -m pep517.build . python -m twine upload dist/*