pax_global_header00006660000000000000000000000064140554300430014510gustar00rootroot0000000000000052 comment=535d439345feff44f4b69c383cc24e7f8d0a8c69 port-for-0.6.1/000077500000000000000000000000001405543004300132645ustar00rootroot00000000000000port-for-0.6.1/.bumpversion.cfg000066400000000000000000000005271405543004300164000ustar00rootroot00000000000000[bumpversion] commit = True tag = True message = "Release {new_version}" current_version = 0.6.1 [bumpversion:file:setup.cfg] search = version = {current_version} replace = version = {new_version} [bumpversion:file:src/port_for/__init__.py] [bumpversion:file:CHANGES.rst] search = unreleased ---------- replace = {new_version} ---------- port-for-0.6.1/.coveragerc000066400000000000000000000001151405543004300154020ustar00rootroot00000000000000[run] omit = src/port_for/_download_ranges.py src/port_for/docopt.py port-for-0.6.1/.github/000077500000000000000000000000001405543004300146245ustar00rootroot00000000000000port-for-0.6.1/.github/dependabot.yml000066400000000000000000000002171405543004300174540ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily time: "04:00" open-pull-requests-limit: 10 port-for-0.6.1/.github/workflows/000077500000000000000000000000001405543004300166615ustar00rootroot00000000000000port-for-0.6.1/.github/workflows/automerge.yml000066400000000000000000000020351405543004300213740ustar00rootroot00000000000000name: Automerge Pull Requests on: pull_request: types: - labeled - unlabeled - synchronize - opened - edited - ready_for_review - reopened - unlocked pull_request_review: types: - submitted check_suite: types: - completed status: {} jobs: automerge: name: Automerge Dependabot runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' steps: - name: 'Wait for status checks' id: waitforstatuschecks uses: "WyriHaximus/github-action-wait-for-status@v1.3" with: ignoreActions: Automerge Dependabot checkInterval: 13 env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - name: Merge pull requests uses: pascalgn/automerge-action@v0.13.1 if: steps.waitforstatuschecks.outputs.status == 'success' env: MERGE_METHOD: "squash" MERGE_LABELS: "" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" port-for-0.6.1/.github/workflows/build.yml000066400000000000000000000011711405543004300205030ustar00rootroot00000000000000name: Test build package on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: name: Build Python 🐍 distributions πŸ“¦ runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install build tools run: pip install build - name: Build a wheel package run: python -m build . - name: Install twine to check the package run: pip install twine - name: Check the package run: twine check dist/* port-for-0.6.1/.github/workflows/linters.yml000066400000000000000000000033351405543004300210700ustar00rootroot00000000000000name: Run linters on: push: branches: [ master ] paths: - '**.py' - .github/workflows/linters.yml - requirements-lint.txt pull_request: branches: [ master ] paths: - '**.py' - .github/workflows/linters.yml - requirements-lint.txt jobs: pydocstyle: runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v2 - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-lint.txt - name: Run pydocstyle run: | pydocstyle src/ tests/ pycodestyle: runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v2 - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-lint.txt - name: Run pydocstyle run: | pycodestyle src/ tests/ black: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - uses: psf/black@main mypy: runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v2 - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-lint.txt - name: Run mypy run: | mypy src/port_for/ tests/ scripts/port-for port-for-0.6.1/.github/workflows/pypi.yml000066400000000000000000000015621405543004300203710ustar00rootroot00000000000000name: Package and publish on: push: tags: - v* jobs: build-n-publish: name: Build and publish Python 🐍 distributions πŸ“¦ to PyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install build tools run: pip install build - name: Build a wheel package run: python -m build . # - name: Publish distribution πŸ“¦ to Test PyPI # uses: pypa/gh-action-pypi-publish@master # with: # password: ${{ secrets.test_pypi_token }} # repository_url: https://test.pypi.org/legacy/ - name: Publish distribution πŸ“¦ to PyPI uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.pypi_token }} verbose: true port-for-0.6.1/.github/workflows/tests-macos.yml000066400000000000000000000021541405543004300216500ustar00rootroot00000000000000name: Run tests on macos on: push: branches: [ master ] paths: - '**.py' - .github/workflows/tests-macos.yml - requirements-test.txt pull_request: branches: [ master ] paths: - '**.py' - .github/workflows/tests-macos.yml - requirements-test.txt jobs: macostests: runs-on: macos-latest strategy: fail-fast: false matrix: python-version: [3.7, 3.8, 3.9, pypy-3.7-v7.3.3] env: OS: macos-latest PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-test.txt - name: Run test run: | pytest --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: ./coverage.xml flags: macos env_vars: OS, PYTHON fail_ci_if_error: true port-for-0.6.1/.github/workflows/tests.yml000066400000000000000000000017671405543004300205610ustar00rootroot00000000000000name: Run tests on: push: branches: [ master ] paths: - '**.py' - .github/workflows/tests.yml - requirements-test.txt pull_request: branches: [ master ] jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: [3.7, 3.8, 3.9, pypy-3.7-v7.3.3] env: OS: ubuntu-latest PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-test.txt - name: Run test run: | pytest --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: ./coverage.xml flags: linux env_vars: OS, PYTHON fail_ci_if_error: true port-for-0.6.1/.gitignore000066400000000000000000000001561405543004300152560ustar00rootroot00000000000000*.pyc build/ dist/ *.egg-info/ .tox .idea htmlcov .coverage .cache .ipynb_checkpoints/ MANIFEST coverage venv/port-for-0.6.1/CHANGES.rst000066400000000000000000000005011405543004300150620ustar00rootroot00000000000000CHANGELOG ========= 0.6.1 ---------- Bugfix ++++++ - Fixed typing definition for get_port function 0.6.0 ---------- Feature +++++++ - Added `get_port` helper that can randomly select open port out of given set, or range-tuple - Added type annotations and compatibility with PEP 561 - Support only python 3.7 and up port-for-0.6.1/LICENSE.txt000066400000000000000000000020431405543004300151060ustar00rootroot00000000000000Copyright (c) 2012 Mikhail Korobov 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. port-for-0.6.1/MANIFEST.in000066400000000000000000000001241405543004300150170ustar00rootroot00000000000000include *.txt include *.rst recursive-include docs *.txt recursive-include scripts *port-for-0.6.1/README.rst000066400000000000000000000100321405543004300147470ustar00rootroot00000000000000======== port-for ======== .. image:: https://img.shields.io/pypi/v/port-for.svg :target: https://pypi.python.org/pypi/port-for :alt: PyPI Version .. image:: http://codecov.io/github/kmike/port-for/coverage.svg?branch=master :target: http://codecov.io/github/kmike/port-for?branch=master :alt: Code Coverage ``port-for`` is a command-line utility and a python library that helps with local TCP ports management. It can find an unused TCP localhost port and remember the association:: $ sudo port-for foo 37987 This can be useful when you are installing a stack of software with multiple parts needing port numbers. .. note:: If you're looking for a temporary port then ``socket.bind((host, 0))`` is your best bet:: >>> import socket >>> s = socket.socket() >>> s.bind(("", 0)) >>> s.getsockname() ('0.0.0.0', 54485) ``port-for`` is necessary when you need *persistent* free local port number. ``port-for`` is the exact opposite of ``s.bind((host, 0))`` in the sense that it shouldn't return ports that ``s.bind((host, 0))`` may return (because such ports are likely to be temporary used by OS). There are several rules ``port-for`` is trying to follow to find and return a new unused port: 1) Port must be unused: ``port-for`` checks this by trying to connect to the port and to bind to it. 2) Port must be IANA unassigned and otherwise not well-known: this is acheived by maintaining unassigned ports list (parsed from IANA and Wikipedia). 3) Port shouldn't be inside ephemeral port range. This is important because ports from ephemeral port range can be assigned temporary by OS (e.g. by machine's IP stack) and this may prevent service restart in some circumstances. ``port-for`` doesn't return ports from ephemeral port ranges configured at the current machine. 4) Other heuristics are also applied: ``port-for`` tries to return a port from larger port ranges; it also doesn't return ports that are too close to well-known ports. Installation ============ System-wide using easy_install (something like ``python-setuptools`` should be installed):: sudo pip install port-for or:: sudo easy_install port-for or inside a virtualenv:: pip install port-for Script usage ============ ``port-for `` script finds an unused port and associates it with ````. Subsequent calls return the same port number. This utility doesn't actually bind the port or otherwise prevents the port from being taken by another software. It tries to select a port that is less likely to be used by another software (and that is unused at the time of calling of course). Utility also makes sure that ``port-for bar`` won't return the same port as ``port-for foo`` on the same machine. :: $ sudo port-for foo 37987 $ port-for foo 37987 You may want to develop some naming conventions (e.g. prefix your app names) in order to enable multiple sites on the same server:: $ sudo port-for example.com/apache 35456 Please note that ``port-for`` script requires read and write access to ``/etc/port-for.conf``. This usually means regular users can read port values but sudo is required to associate a new port. List all associated ports:: $ port-for --list foo: 37987 example.com/apache: 35456 Remove an association:: $ sudo port-for --unbind foo $ port-for --list example.com/apache: 35456 Library usage ============= :: >>> import port_for >>> port_for.select_random() 37774 >>> port_for.select_random() 48324 >>> 80 in port_for.available_good_ports() False >>> port_for.get_port() 34455 >>> port_for.get_port("1234") 1234 >>> port_for.get_port((2000, 3000)) 2345 >>> port_for.get_port({4001, 4003, 4005}) 4005 >>> port_for.get_port([{4000, 4001}, (4100, 4200)]) 4111 Dig into source code for more. Contributing ============ Development happens at github: https://github.com/kmike/port-for/ Issue tracker: https://github.com/kmike/port-for/issues/newport-for-0.6.1/mypy.ini000066400000000000000000000011641405543004300147650ustar00rootroot00000000000000[mypy] allow_redefinition = False allow_untyped_globals = False check_untyped_defs = True disallow_incomplete_defs = True disallow_subclassing_any = True disallow_untyped_calls = True disallow_untyped_decorators = True disallow_untyped_defs = True follow_imports = silent ignore_missing_imports = False implicit_reexport = False no_implicit_optional = True pretty = True show_error_codes = True strict_equality = True warn_no_return = True warn_return_any = True warn_unreachable = True warn_unused_ignores = True # Bundled third-party package. [mypy-port_for.docopt.*] check_untyped_defs = False disallow_untyped_defs = False port-for-0.6.1/pyproject.toml000066400000000000000000000002631405543004300162010ustar00rootroot00000000000000[build-system] requires = ["setuptools >= 40.6.0", "wheel"] build-backend = "setuptools.build_meta" [tool.black] line-length = 80 target-version = ['py38'] include = '.*\.pyi?$' port-for-0.6.1/requirements-lint.txt000066400000000000000000000001531405543004300175130ustar00rootroot00000000000000# linters pycodestyle==2.7.0 pydocstyle==6.1.1 pygments black==21.5b2 mypy==0.812 -r requirements-test.txt port-for-0.6.1/requirements-test.txt000066400000000000000000000003551405543004300175300ustar00rootroot00000000000000# test runs requirements (versions we'll be testing against) - automatically updated mock==4.0.3 pytest==6.2.4 # tests framework used pytest-cov==2.12.0 # coverage reports to verify tests quality coverage==5.5 # pytest-cov -e .[tests] port-for-0.6.1/scripts/000077500000000000000000000000001405543004300147535ustar00rootroot00000000000000port-for-0.6.1/scripts/port-for000066400000000000000000000031501405543004300164450ustar00rootroot00000000000000#!/usr/bin/env python """ port-for is a command-line utility that helps with local TCP ports management. It finds 'good' unused TCP localhost port and remembers the association. Usage: port-for port-for --bind port-for --bind --port port-for --port port-for --unbind port-for --list port-for --version port-for --help Options: -h --help Show this screen. -v, --version Show version. -b FOO, --bind FOO Find and return a port for FOO; this is an alias for 'port-for FOO'. -p PORT, --port PORT (Optional) specific port number for the --bind command. -u FOO, --unbind FOO Remove association for FOO. -l, --list List all associated ports. """ import sys from typing import Optional import port_for from port_for.docopt import docopt store = port_for.PortStore() def _list() -> None: for app, port in store.bound_ports(): sys.stdout.write("%s: %s\n" % (app, port)) def _bind(app: str, port: Optional[str] = None) -> None: bound_port = store.bind_port(app, port) sys.stdout.write("%s\n" % bound_port) def _unbind(app: str) -> None: store.unbind_port(app) if __name__ == "__main__": args = docopt( __doc__, version="port-for %s" % port_for.__version__, ) # type: ignore[no-untyped-call] if args[""]: _bind(args[""], args["--port"]) elif args["--bind"]: _bind(args["--bind"], args["--port"]) elif args["--list"]: _list() elif args["--unbind"]: _unbind(args["--unbind"]) port-for-0.6.1/setup.cfg000066400000000000000000000032601405543004300151060ustar00rootroot00000000000000[metadata] name = port-for version = 0.6.1 url = https://github.com/kmike/port-for/ description = Utility that helps with local TCP ports management. It can find an unused TCP localhost port and remember the association. long_description = file: README.rst, CHANGES.rst long_description_content_type = text/x-rst keywords = port, posix license = MIT license author = Mikhail Korobov author_email = kmike84@gmail.com maintainer = Grzegorz ŚliwiΕ„ski maintainer_email = fizyk+pypi@fizyk.net.pl classifiers = Development Status :: 4 - Beta Intended Audience :: Developers Intended Audience :: System Administrators License :: OSI Approved :: MIT License 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 Operating System :: POSIX Topic :: System :: Installation/Setup Topic :: System :: Systems Administration Topic :: Internet :: WWW/HTTP :: Site Management [options] zip_safe = False include_package_data = True python_requires = >= 3.7 packages = find: package_dir = =src scripts = scripts/port-for [options.packages.find] where = src [options.extras_require] tests = pytest pytest-cov mock [options.package_data] port_for = py.typed [pycodestyle] max-line-length = 80 exclude = docs/*,build/*,venv/* [pydocstyle] ignore = D203,D212 match = '(?!docs|build|venv).*\.py' [tool:pytest] addopts = -vvv --capture=no --showlocals --cov src/port_for --cov tests --ignore src/port_for/_download_ranges.py testpaths = tests/ filterwarnings = error xfail_strict = Trueport-for-0.6.1/setup.py000077500000000000000000000000751405543004300150030ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup() port-for-0.6.1/src/000077500000000000000000000000001405543004300140535ustar00rootroot00000000000000port-for-0.6.1/src/port_for/000077500000000000000000000000001405543004300157055ustar00rootroot00000000000000port-for-0.6.1/src/port_for/__init__.py000066400000000000000000000010441405543004300200150ustar00rootroot00000000000000# -*- coding: utf-8 -*- __version__ = "0.6.1" from ._ranges import UNASSIGNED_RANGES from .api import ( available_good_ports, available_ports, is_available, good_port_ranges, port_is_used, select_random, get_port, ) from .store import PortStore from .exceptions import PortForException __all__ = ( "UNASSIGNED_RANGES", "available_good_ports", "available_ports", "is_available", "good_port_ranges", "port_is_used", "select_random", "get_port", "PortStore", "PortForException", ) port-for-0.6.1/src/port_for/_download_ranges.py000066400000000000000000000061301405543004300215640ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ This module/script is for updating port_for._ranges with recent information from IANA and Wikipedia. """ import sys import os import re import datetime from urllib.request import Request, urlopen from xml.etree import ElementTree from typing import Set, Iterator, Iterable, Tuple from port_for.utils import to_ranges, ranges_to_set name = os.path.abspath( os.path.normpath(os.path.join(os.path.dirname(__file__), "..")) ) sys.path.insert(0, name) IANA_DOWNLOAD_URL = ( "https://www.iana.org/assignments" "/service-names-port-numbers/service-names-port-numbers.xml" ) IANA_NS = "http://www.iana.org/assignments" WIKIPEDIA_PAGE = "http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers" def _write_unassigned_ranges(out_filename: str) -> None: """ Downloads ports data from IANA & Wikipedia and converts it to a python module. This function is used to generate _ranges.py. """ with open(out_filename, "wt") as f: f.write( "# auto-generated by port_for._download_ranges (%s)\n" % datetime.date.today() ) f.write("UNASSIGNED_RANGES = [\n") for range in to_ranges(sorted(list(_unassigned_ports()))): f.write(" (%d, %d),\n" % range) f.write("]\n") def _unassigned_ports() -> Set[int]: """Return a set of all unassigned ports (according to IANA and Wikipedia)""" free_ports = ranges_to_set(_parse_ranges(_iana_unassigned_port_ranges())) known_ports = ranges_to_set(_wikipedia_known_port_ranges()) return free_ports.difference(known_ports) def _wikipedia_known_port_ranges() -> Iterator[Tuple[int, int]]: """ Returns used port ranges according to Wikipedia page. This page contains unofficial well-known ports. """ req = Request(WIKIPEDIA_PAGE, headers={"User-Agent": "Magic Browser"}) page = urlopen(req).read().decode("utf8") # just find all numbers in table cells ports = re.findall(r"((\d+)(\W(\d+))?)", page, re.U) return ((int(p[1]), int(p[3] if p[3] else p[1])) for p in ports) def _iana_unassigned_port_ranges() -> Iterator[str]: """ Returns unassigned port ranges according to IANA. """ page = urlopen(IANA_DOWNLOAD_URL).read() xml = ElementTree.fromstring(page) records = xml.findall("{%s}record" % IANA_NS) for record in records: description_el = record.find("{%s}description" % IANA_NS) assert description_el is not None description = description_el.text if description == "Unassigned": number_el = record.find("{%s}number" % IANA_NS) assert number_el is not None numbers = number_el.text assert numbers is not None yield numbers def _parse_ranges(ranges: Iterable[str]) -> Iterator[Tuple[int, int]]: """Converts a list of string ranges to a list of [low, high] tuples.""" for txt in ranges: if "-" in txt: low, high = txt.split("-") else: low, high = txt, txt yield int(low), int(high) if __name__ == "__main__": _write_unassigned_ranges("_ranges.py") port-for-0.6.1/src/port_for/_ranges.py000066400000000000000000000316051405543004300177020ustar00rootroot00000000000000# auto-generated by port_for._download_ranges (2017-07-19) UNASSIGNED_RANGES = [ (28, 28), (30, 30), (32, 32), (34, 34), (36, 36), (60, 60), (258, 258), (272, 279), (285, 285), (288, 299), (301, 307), (325, 332), (334, 343), (703, 703), (708, 708), (717, 728), (732, 740), (743, 743), (745, 746), (755, 757), (766, 766), (768, 768), (778, 779), (781, 781), (784, 799), (803, 807), (809, 809), (811, 827), (834, 842), (844, 846), (849, 852), (855, 859), (863, 872), (874, 885), (889, 896), (899, 899), (904, 909), (1002, 1007), (1009, 1009), (1491, 1491), (2194, 2194), (2259, 2259), (2378, 2378), (2693, 2693), (2794, 2794), (2873, 2873), (3092, 3092), (3126, 3126), (3546, 3546), (3694, 3694), (3994, 3994), (4048, 4048), (4120, 4120), (4144, 4144), (4194, 4196), (4315, 4315), (4317, 4319), (4332, 4332), (4337, 4339), (4363, 4365), (4367, 4367), (4380, 4388), (4397, 4399), (4424, 4424), (4434, 4440), (4459, 4483), (4489, 4499), (4501, 4501), (4539, 4544), (4561, 4562), (4564, 4565), (4571, 4572), (4574, 4589), (4606, 4609), (4641, 4657), (4693, 4699), (4705, 4710), (4712, 4712), (4714, 4724), (4734, 4736), (4748, 4748), (4757, 4773), (4775, 4783), (4792, 4799), (4805, 4826), (4828, 4836), (4852, 4866), (4872, 4875), (4886, 4893), (4895, 4898), (4903, 4911), (4916, 4935), (4938, 4939), (4943, 4948), (4954, 4968), (4971, 4979), (4981, 4983), (4992, 4998), (5016, 5019), (5035, 5036), (5038, 5041), (5076, 5077), (5088, 5089), (5095, 5098), (5108, 5110), (5113, 5113), (5118, 5119), (5122, 5123), (5126, 5132), (5138, 5144), (5147, 5149), (5158, 5160), (5169, 5171), (5173, 5189), (5204, 5208), (5210, 5214), (5216, 5220), (5238, 5241), (5244, 5244), (5255, 5263), (5266, 5268), (5273, 5279), (5283, 5297), (5311, 5311), (5316, 5316), (5319, 5319), (5322, 5342), (5345, 5348), (5365, 5393), (5395, 5396), (5438, 5442), (5444, 5444), (5446, 5449), (5451, 5452), (5460, 5460), (5466, 5467), (5469, 5469), (5476, 5479), (5482, 5494), (5496, 5497), (5508, 5516), (5518, 5549), (5551, 5552), (5558, 5564), (5570, 5572), (5576, 5578), (5587, 5596), (5606, 5617), (5619, 5626), (5640, 5645), (5647, 5655), (5657, 5665), (5668, 5669), (5685, 5686), (5690, 5692), (5694, 5695), (5697, 5699), (5701, 5704), (5706, 5712), (5731, 5740), (5749, 5749), (5751, 5754), (5756, 5756), (5758, 5765), (5772, 5776), (5778, 5779), (5788, 5792), (5795, 5799), (5801, 5812), (5815, 5840), (5843, 5858), (5860, 5862), (5864, 5867), (5869, 5880), (5882, 5882), (5884, 5899), (5901, 5909), (5914, 5930), (5932, 5937), (5939, 5962), (5964, 5967), (5970, 5983), (5994, 5998), (6067, 6067), (6078, 6079), (6089, 6098), (6119, 6120), (6125, 6128), (6131, 6132), (6134, 6135), (6137, 6139), (6150, 6158), (6164, 6199), (6202, 6208), (6210, 6221), (6223, 6224), (6226, 6226), (6228, 6229), (6231, 6239), (6245, 6250), (6254, 6254), (6256, 6256), (6258, 6259), (6261, 6261), (6263, 6266), (6270, 6299), (6302, 6305), (6307, 6314), (6318, 6319), (6323, 6323), (6327, 6342), (6345, 6345), (6348, 6349), (6351, 6354), (6356, 6359), (6361, 6362), (6364, 6369), (6371, 6378), (6380, 6381), (6383, 6388), (6391, 6399), (6411, 6416), (6422, 6431), (6433, 6435), (6438, 6441), (6447, 6454), (6457, 6463), (6465, 6470), (6472, 6479), (6490, 6499), (6504, 6504), (6512, 6512), (6516, 6521), (6524, 6540), (6545, 6546), (6552, 6555), (6557, 6557), (6559, 6559), (6562, 6565), (6569, 6570), (6572, 6578), (6584, 6599), (6603, 6618), (6630, 6631), (6637, 6639), (6641, 6652), (6654, 6654), (6658, 6659), (6674, 6677), (6680, 6686), (6691, 6695), (6698, 6698), (6700, 6702), (6707, 6713), (6717, 6766), (6772, 6776), (6779, 6782), (6792, 6800), (6802, 6816), (6818, 6830), (6832, 6840), (6843, 6849), (6851, 6867), (6870, 6880), (10011, 10019), (10021, 10023), (10026, 10041), (10043, 10049), (10052, 10054), (10056, 10079), (10082, 10099), (10105, 10106), (10108, 10109), (10112, 10112), (10118, 10124), (10126, 10127), (10130, 10159), (10163, 10171), (10173, 10199), (10205, 10211), (10213, 10251), (10254, 10259), (10262, 10287), (10289, 10300), (10303, 10307), (10309, 10320), (10322, 10438), (10440, 10479), (10481, 10499), (10501, 10504), (10506, 10513), (10515, 10539), (10545, 10547), (10549, 10630), (10632, 10799), (10801, 10804), (10806, 10808), (10811, 10822), (10824, 10859), (10861, 10879), (10881, 10890), (10892, 10932), (10934, 10989), (10991, 10999), (11002, 11022), (11024, 11094), (11096, 11102), (11107, 11107), (11113, 11154), (11156, 11160), (11166, 11170), (11176, 11200), (11203, 11207), (11209, 11210), (11212, 11213), (11216, 11234), (11236, 11310), (11312, 11318), (11322, 11366), (11368, 11370), (11372, 11429), (11431, 11488), (11490, 11575), (11577, 11599), (11601, 11622), (11624, 11719), (11721, 11722), (11724, 11750), (11752, 11752), (11754, 11795), (11797, 11875), (11878, 11949), (11952, 11966), (11968, 11996), (12014, 12029), (12033, 12108), (12110, 12120), (12122, 12167), (12169, 12171), (12173, 12200), (12202, 12221), (12224, 12299), (12301, 12301), (12303, 12320), (12323, 12344), (12346, 12442), (12444, 12488), (12490, 12752), (12754, 12864), (12866, 12974), (12976, 13000), (13002, 13007), (13009, 13074), (13076, 13159), (13161, 13194), (13197, 13215), (13219, 13222), (13225, 13399), (13401, 13719), (13723, 13723), (13725, 13781), (13784, 13784), (13787, 13817), (13824, 13893), (13895, 13928), (13931, 13999), (14003, 14032), (14035, 14140), (14144, 14144), (14146, 14148), (14151, 14153), (14155, 14249), (14251, 14413), (14415, 14499), (14501, 14549), (14551, 14566), (14568, 14899), (14901, 14935), (14938, 14999), (15001, 15001), (15003, 15117), (15119, 15344), (15346, 15362), (15364, 15554), (15557, 15566), (15568, 15659), (15661, 15739), (15741, 15997), (16004, 16019), (16022, 16079), (16081, 16160), (16163, 16199), (16201, 16224), (16226, 16249), (16251, 16260), (16262, 16299), (16301, 16308), (16312, 16359), (16362, 16366), (16369, 16383), (16388, 16392), (16473, 16481), (16483, 16566), (16568, 16618), (16620, 16664), (16667, 16788), (16790, 16899), (16901, 16949), (16951, 16990), (16996, 17006), (17008, 17010), (17012, 17183), (17186, 17218), (17226, 17233), (17236, 17499), (17501, 17554), (17556, 17728), (17730, 17753), (17757, 17776), (17778, 17999), (18001, 18090), (18093, 18103), (18105, 18135), (18137, 18180), (18188, 18199), (18202, 18205), (18207, 18240), (18244, 18261), (18263, 18299), (18302, 18305), (18307, 18332), (18334, 18399), (18402, 18462), (18464, 18504), (18507, 18604), (18607, 18633), (18636, 18667), (18669, 18768), (18770, 18880), (18882, 18887), (18889, 18999), (19002, 19006), (19008, 19019), (19021, 19131), (19133, 19149), (19151, 19190), (19192, 19193), (19195, 19219), (19221, 19225), (19227, 19282), (19284, 19293), (19296, 19301), (19303, 19314), (19316, 19397), (19399, 19409), (19413, 19538), (19542, 19787), (19789, 19811), (19815, 19997), (20004, 20004), (20006, 20011), (20015, 20033), (20035, 20045), (20047, 20047), (20050, 20056), (20058, 20166), (20168, 20201), (20203, 20221), (20223, 20479), (20481, 20559), (20561, 20594), (20596, 20669), (20671, 20701), (20703, 20719), (20721, 20789), (20791, 20807), (20809, 20998), (21001, 21009), (21011, 21024), (21026, 21220), (21222, 21552), (21555, 21589), (21591, 21799), (21801, 21844), (21850, 21999), (22006, 22124), (22126, 22127), (22129, 22135), (22137, 22221), (22223, 22272), (22274, 22304), (22306, 22334), (22336, 22342), (22344, 22346), (22348, 22348), (22352, 22536), (22538, 22554), (22556, 22762), (22764, 22799), (22801, 22950), (22952, 22999), (23006, 23052), (23054, 23072), (23074, 23271), (23273, 23283), (23285, 23293), (23295, 23332), (23334, 23398), (23403, 23455), (23458, 23512), (23514, 23545), (23547, 23999), (24007, 24241), (24243, 24248), (24250, 24320), (24323, 24385), (24387, 24440), (24442, 24443), (24445, 24464), (24466, 24553), (24555, 24576), (24578, 24665), (24667, 24675), (24679, 24679), (24681, 24753), (24755, 24799), (24801, 24841), (24843, 24849), (24851, 24921), (24923, 24999), (25011, 25104), (25106, 25470), (25472, 25559), (25561, 25564), (25566, 25569), (25571, 25574), (25577, 25603), (25605, 25792), (25794, 25825), (25827, 25827), (25841, 25887), (25889, 25899), (25904, 25953), (25956, 25998), (26001, 26132), (26134, 26207), (26209, 26256), (26258, 26259), (26265, 26485), (26488, 26488), (26490, 26899), (26902, 26949), (26951, 26999), (27051, 27344), (27346, 27373), (27375, 27441), (27443, 27499), (27911, 27949), (27951, 27959), (27970, 27998), (28002, 28014), (28016, 28118), (28120, 28199), (28201, 28239), (28241, 28588), (28590, 28769), (28772, 28784), (28787, 28851), (28853, 28909), (28911, 28959), (28961, 28999), (29001, 29069), (29071, 29117), (29119, 29166), (29170, 29899), (29902, 29919), (29921, 29998), (30005, 30099), (30101, 30259), (30261, 30563), (30565, 30831), (30833, 30998), (31000, 31015), (31017, 31019), (31021, 31028), (31030, 31336), (31338, 31399), (31401, 31415), (31417, 31437), (31439, 31456), (31458, 31619), (31621, 31684), (31686, 31764), (31766, 31947), (31950, 32033), (32035, 32136), (32138, 32248), (32250, 32399), (32401, 32482), (32484, 32634), (32637, 32763), (32765, 32766), (32778, 32800), (32802, 32810), (32812, 32886), (32888, 32895), (32897, 32975), (32977, 33059), (33061, 33122), (33124, 33330), (33332, 33332), (33335, 33433), (33435, 33655), (33657, 33847), (33849, 33999), (34001, 34248), (34250, 34377), (34380, 34566), (34568, 34961), (34965, 34979), (34981, 34999), (35007, 35099), (35101, 35353), (35358, 36000), (36002, 36410), (36413, 36421), (36425, 36442), (36445, 36461), (36463, 36523), (36525, 36601), (36603, 36699), (36701, 36864), (36866, 37474), (37476, 37482), (37484, 37600), (37602, 37653), (37655, 37999), (38003, 38200), (38204, 38411), (38413, 38421), (38423, 38471), (38473, 38799), (38801, 38864), (38866, 39680), (39682, 39999), (40001, 40022), (40024, 40403), (40405, 40840), (40844, 40852), (40854, 41110), (41112, 41120), (41122, 41229), (41231, 41793), (41798, 42507), (42511, 42999), (43001, 43593), (43596, 44320), (44323, 44404), (44406, 44443), (44445, 44543), (44545, 44552), (44554, 44599), (44601, 44817), (44819, 44899), (44901, 44999), (45003, 45044), (45046, 45053), (45055, 45513), (45515, 45677), (45679, 45823), (45826, 45965), (45967, 46335), (46337, 46997), (47002, 47099), (47101, 47556), (47558, 47623), (47625, 47805), (47807, 47807), (47810, 47999), (48006, 48048), (48051, 48127), (48130, 48555), (48557, 48618), (48620, 48652), (48654, 48999), (49002, 49150), ] port-for-0.6.1/src/port_for/api.py000066400000000000000000000130711405543004300170320ustar00rootroot00000000000000# -*- coding: utf-8 -*- import contextlib import socket import errno import random from itertools import chain from typing import Optional, Set, List, Tuple, Iterable, TypeVar, Type, Union from port_for import ephemeral, utils from ._ranges import UNASSIGNED_RANGES from .exceptions import PortForException SYSTEM_PORT_RANGE = (0, 1024) def select_random( ports: Optional[Set[int]] = None, exclude_ports: Optional[Iterable[int]] = None, ) -> int: """ Returns random unused port number. """ if ports is None: ports = available_good_ports() if exclude_ports is None: exclude_ports = set() ports.difference_update(set(exclude_ports)) for port in random.sample(tuple(ports), min(len(ports), 100)): if not port_is_used(port): return port raise PortForException("Can't select a port") def is_available(port: int) -> bool: """ Returns if port is good to choose. """ return port in available_ports() and not port_is_used(port) def available_ports( low: int = 1024, high: int = 65535, exclude_ranges: Optional[List[Tuple[int, int]]] = None, ) -> Set[int]: """ Returns a set of possible ports (excluding system, ephemeral and well-known ports). Pass ``high`` and/or ``low`` to limit the port range. """ if exclude_ranges is None: exclude_ranges = [] available = utils.ranges_to_set(UNASSIGNED_RANGES) exclude = utils.ranges_to_set( ephemeral.port_ranges() + exclude_ranges + [SYSTEM_PORT_RANGE, (SYSTEM_PORT_RANGE[1], low), (high, 65536)] ) return available.difference(exclude) def good_port_ranges( ports: Optional[Set[int]] = None, min_range_len: int = 20, border: int = 3 ) -> List[Tuple[int, int]]: """ Returns a list of 'good' port ranges. Such ranges are large and don't contain ephemeral or well-known ports. Ranges borders are also excluded. """ min_range_len += border * 2 if ports is None: ports = available_ports() ranges = utils.to_ranges(list(ports)) lenghts = sorted([(r[1] - r[0], r) for r in ranges], reverse=True) long_ranges = [ length[1] for length in lenghts if length[0] >= min_range_len ] without_borders = [ (low + border, high - border) for low, high in long_ranges ] return without_borders def available_good_ports(min_range_len: int = 20, border: int = 3) -> Set[int]: return utils.ranges_to_set( good_port_ranges(min_range_len=min_range_len, border=border) ) def port_is_used(port: int, host: str = "127.0.0.1") -> bool: """ Returns if port is used. Port is considered used if the current process can't bind to it or the port doesn't refuse connections. """ unused = _can_bind(port, host) and _refuses_connection(port, host) return not unused def _can_bind(port: int, host: str) -> bool: sock = socket.socket() with contextlib.closing(sock): try: sock.bind((host, port)) except socket.error: return False return True def _refuses_connection(port: int, host: str) -> bool: sock = socket.socket() with contextlib.closing(sock): sock.settimeout(1) err = sock.connect_ex((host, port)) return err == errno.ECONNREFUSED T = TypeVar("T") def filter_by_type(lst: Iterable, type_of: Type[T]) -> List[T]: """Returns a list of elements with given type.""" return [e for e in lst if isinstance(e, type_of)] def get_port( ports: Union[ None, str, int, Tuple[int, int], Set[int], List[str], List[int], List[Tuple[int, int]], List[Set[int]], List[Union[Set[int], Tuple[int, int]]], List[Union[str, int, Tuple[int, int], Set[int]]], ] ) -> Optional[int]: """ Retuns a random available port. If there's only one port passed (e.g. 5000 or '5000') function does not check if port is available. If there's -1 passed as an argument, function returns None. :param str|int|tuple|set|list ports: exact port (e.g. '8000', 8000) randomly selected port (None) - any random available port [(2000,3000)] or (2000,3000) - random available port from a given range [{4002,4003}] or {4002,4003} - random of 4002 or 4003 ports [(2000,3000), {4002,4003}] -random of given range and set :returns: a random free port :raises: ValueError """ if ports == -1: return None elif not ports: return select_random(None) try: return int(ports) # type: ignore[arg-type] except TypeError: pass ports_set: Set[int] = set() try: if not isinstance(ports, list): ports = [ports] ranges: Set[int] = utils.ranges_to_set( filter_by_type(ports, tuple) # type: ignore[arg-type] ) nums: Set[int] = set(filter_by_type(ports, int)) sets: Set[int] = set( chain( *filter_by_type( ports, (set, frozenset) # type: ignore[arg-type] ) ) ) ports_set = ports_set.union(ranges, sets, nums) except ValueError: raise PortForException( "Unknown format of ports: %s.\n" 'You should provide a ports range "[(4000,5000)]"' 'or "(4000,5000)" or a comma-separated ports set' '"[{4000,5000,6000}]" or list of ints "[400,5000,6000,8000]"' 'or all of them "[(20000, 30000), {48889, 50121}, 4000, 4004]"' % (ports,) ) return select_random(ports_set) port-for-0.6.1/src/port_for/docopt.py000066400000000000000000000367371405543004300175670ustar00rootroot00000000000000from copy import copy import sys import re class DocoptLanguageError(Exception): """Error in construction of usage-message by developer.""" class DocoptExit(SystemExit): """Exit in case user invoked program with incorrect arguments.""" usage = "" def __init__(self, message=""): SystemExit.__init__(self, (message + "\n" + self.usage).strip()) class Pattern(object): def __init__(self, *children): self.children = list(children) def __eq__(self, other): return repr(self) == repr(other) def __hash__(self): return hash(repr(self)) def __repr__(self): return "%s(%s)" % ( self.__class__.__name__, ", ".join(repr(a) for a in self.children), ) @property def flat(self): if not hasattr(self, "children"): return [self] return sum([c.flat for c in self.children], []) def fix(self): self.fix_identities() self.fix_list_arguments() return self def fix_identities(self, uniq=None): """Make pattern-tree tips point to same object if they are equal.""" if not hasattr(self, "children"): return self uniq = list(set(self.flat)) if uniq is None else uniq for i, c in enumerate(self.children): if not hasattr(c, "children"): assert c in uniq self.children[i] = uniq[uniq.index(c)] else: c.fix_identities(uniq) def fix_list_arguments(self): """Find arguments that should accumulate values and fix them.""" either = [list(c.children) for c in self.either.children] for case in either: case = [c for c in case if case.count(c) > 1] for a in [e for e in case if type(e) == Argument]: a.value = [] return self @property def either(self): """Transform pattern into an equivalent, with only top-level Either.""" # Currently the pattern will not be equivalent, but more "narrow", # although good enough to reason about list arguments. if not hasattr(self, "children"): return Either(Required(self)) else: ret = [] groups = [[self]] while groups: children = groups.pop(0) types = [type(c) for c in children] if Either in types: either = [c for c in children if type(c) is Either][0] children.pop(children.index(either)) for c in either.children: groups.append([c] + children) elif Required in types: required = [c for c in children if type(c) is Required][0] children.pop(children.index(required)) groups.append(list(required.children) + children) elif Optional in types: optional = [c for c in children if type(c) is Optional][0] children.pop(children.index(optional)) groups.append(list(optional.children) + children) elif OneOrMore in types: oneormore = [c for c in children if type(c) is OneOrMore][0] children.pop(children.index(oneormore)) groups.append(list(oneormore.children) * 2 + children) else: ret.append(children) return Either(*[Required(*e) for e in ret]) class Argument(Pattern): def __init__(self, name, value=None): self.name = name self.value = value def match(self, left, collected=None): collected = [] if collected is None else collected args = [arg_left for arg_left in left if type(arg_left) is Argument] if not len(args): return False, left, collected left.remove(args[0]) if type(self.value) is not list: return True, left, collected + [Argument(self.name, args[0].value)] same_name = [ a for a in collected if type(a) is Argument and a.name == self.name ] if len(same_name): same_name[0].value += [args[0].value] return True, left, collected else: return ( True, left, collected + [Argument(self.name, [args[0].value])], ) def __repr__(self): return "Argument(%r, %r)" % (self.name, self.value) class Command(Pattern): def __init__(self, name, value=False): self.name = name self.value = value def match(self, left, collected=None): collected = [] if collected is None else collected args = [arg_left for arg_left in left if type(arg_left) is Argument] if not len(args) or args[0].value != self.name: return False, left, collected left.remove(args[0]) return True, left, collected + [Command(self.name, True)] def __repr__(self): return "Command(%r, %r)" % (self.name, self.value) class Option(Pattern): def __init__(self, short=None, long=None, argcount=0, value=False): assert argcount in (0, 1) self.short, self.long = short, long self.argcount, self.value = argcount, value self.value = None if not value and argcount else value # HACK @classmethod def parse(class_, option_description): short, long, argcount, value = None, None, 0, False options, _, description = option_description.strip().partition(" ") options = options.replace(",", " ").replace("=", " ") for s in options.split(): if s.startswith("--"): long = s elif s.startswith("-"): short = s else: argcount = 1 if argcount: matched = re.findall(r"\[default: (.*)\]", description, flags=re.I) value = matched[0] if matched else None return class_(short, long, argcount, value) def match(self, left, collected=None): collected = [] if collected is None else collected left_ = [] for arg_left in left: # if this is so greedy, how to handle OneOrMore then? if not ( type(arg_left) is Option and (self.short, self.long) == (arg_left.short, arg_left.long) ): left_.append(arg_left) return (left != left_), left_, collected @property def name(self): return self.long or self.short def __repr__(self): return "Option(%r, %r, %r, %r)" % ( self.short, self.long, self.argcount, self.value, ) class AnyOptions(Pattern): def match(self, left, collected=None): collected = [] if collected is None else collected left_ = [opt_left for opt_left in left if not type(opt_left) == Option] return (left != left_), left_, collected class Required(Pattern): def match(self, left, collected=None): collected = [] if collected is None else collected copied_left = copy(left) c = copy(collected) for p in self.children: matched, copied_left, c = p.match(copied_left, c) if not matched: return False, left, collected return True, copied_left, c class Optional(Pattern): def match(self, left, collected=None): collected = [] if collected is None else collected left = copy(left) for p in self.children: m, left, collected = p.match(left, collected) return True, left, collected class OneOrMore(Pattern): def match(self, left, collected=None): assert len(self.children) == 1 collected = [] if collected is None else collected pattern_left = copy(left) c = copy(collected) l_ = None matched = True times = 0 while matched: # could it be that something didn't match but # changed pattern_left or c? matched, pattern_left, c = self.children[0].match(pattern_left, c) times += 1 if matched else 0 if l_ == pattern_left: break l_ = copy(pattern_left) if times >= 1: return True, pattern_left, c return False, left, collected class Either(Pattern): def match(self, left, collected=None): collected = [] if collected is None else collected outcomes = [] for p in self.children: matched, _, _ = outcome = p.match(copy(left), copy(collected)) if matched: outcomes.append(outcome) if outcomes: return min(outcomes, key=lambda outcome: len(outcome[1])) return False, left, collected class TokenStream(list): def __init__(self, source, error): self += source.split() if type(source) is str else source self.error = error def move(self): return self.pop(0) if len(self) else None def current(self): return self[0] if len(self) else None def parse_long(tokens, options): raw, eq, value = tokens.move().partition("=") value = None if eq == value == "" else value opt = [o for o in options if o.long and o.long.startswith(raw)] if len(opt) < 1: if tokens.error is DocoptExit: raise tokens.error("%s is not recognized" % raw) else: o = Option(None, raw, (1 if eq == "=" else 0)) options.append(o) return [o] if len(opt) > 1: raise tokens.error( "%s is not a unique prefix: %s?" % (raw, ", ".join("%s" % o.long for o in opt)) ) opt = copy(opt[0]) if opt.argcount == 1: if value is None: if tokens.current() is None: raise tokens.error("%s requires argument" % opt.name) value = tokens.move() elif value is not None: raise tokens.error("%s must not have an argument" % opt.name) opt.value = value or True return [opt] def parse_shorts(tokens, options): raw = tokens.move()[1:] parsed = [] while raw != "": opt = [ o for o in options if o.short and o.short.lstrip("-").startswith(raw[0]) ] if len(opt) > 1: raise tokens.error( "-%s is specified ambiguously %d times" % (raw[0], len(opt)) ) if len(opt) < 1: if tokens.error is DocoptExit: raise tokens.error("-%s is not recognized" % raw[0]) else: o = Option("-" + raw[0], None) options.append(o) parsed.append(o) raw = raw[1:] continue opt = copy(opt[0]) raw = raw[1:] if opt.argcount == 0: value = True else: if raw == "": if tokens.current() is None: raise tokens.error("-%s requires argument" % opt.short[0]) raw = tokens.move() value, raw = raw, "" opt.value = value parsed.append(opt) return parsed def parse_pattern(source, options): tokens = TokenStream( re.sub(r"([\[\]\(\)\|]|\.\.\.)", r" \1 ", source), DocoptLanguageError ) result = parse_expr(tokens, options) if tokens.current() is not None: raise tokens.error("unexpected ending: %r" % " ".join(tokens)) return Required(*result) def parse_expr(tokens, options): """expr ::= seq ( '|' seq )* ;""" seq = parse_seq(tokens, options) if tokens.current() != "|": return seq result = [Required(*seq)] if len(seq) > 1 else seq while tokens.current() == "|": tokens.move() seq = parse_seq(tokens, options) result += [Required(*seq)] if len(seq) > 1 else seq return [Either(*result)] if len(result) > 1 else result def parse_seq(tokens, options): """seq ::= ( atom [ '...' ] )* ;""" result = [] while tokens.current() not in [None, "]", ")", "|"]: atom = parse_atom(tokens, options) if tokens.current() == "...": atom = [OneOrMore(*atom)] tokens.move() result += atom return result def parse_atom(tokens, options): """atom ::= '(' expr ')' | '[' expr ']' | 'options' | long | shorts | argument | command ; """ token = tokens.current() result = [] if token == "(": tokens.move() result = [Required(*parse_expr(tokens, options))] if tokens.move() != ")": raise tokens.error("Unmatched '('") return result elif token == "[": tokens.move() result = [Optional(*parse_expr(tokens, options))] if tokens.move() != "]": raise tokens.error("Unmatched '['") return result elif token == "options": tokens.move() return [AnyOptions()] elif token.startswith("--") and token != "--": return parse_long(tokens, options) elif token.startswith("-") and token not in ("-", "--"): return parse_shorts(tokens, options) elif token.startswith("<") and token.endswith(">") or token.isupper(): return [Argument(tokens.move())] else: return [Command(tokens.move())] def parse_args(source, options): tokens = TokenStream(source, DocoptExit) options = copy(options) parsed = [] while tokens.current() is not None: if tokens.current() == "--": return parsed + [Argument(None, v) for v in tokens] elif tokens.current().startswith("--"): parsed += parse_long(tokens, options) elif tokens.current().startswith("-") and tokens.current() != "-": parsed += parse_shorts(tokens, options) else: parsed.append(Argument(None, tokens.move())) return parsed def parse_doc_options(doc): return [Option.parse("-" + s) for s in re.split("^ *-|\n *-", doc)[1:]] def printable_usage(doc): usage_split = re.split(r"([Uu][Ss][Aa][Gg][Ee]:)", doc) if len(usage_split) < 3: raise DocoptLanguageError('"usage:" (case-insensitive) not found.') if len(usage_split) > 3: raise DocoptLanguageError('More than one "usage:" (case-insensitive).') return re.split(r"\n\s*\n", "".join(usage_split[1:]))[0].strip() def formal_usage(printable_usage): pu = printable_usage.split()[1:] # split and drop "usage:" return " ".join("|" if s == pu[0] else s for s in pu[1:]) def extras(help, version, options, doc): if help and any((o.name in ("-h", "--help")) and o.value for o in options): print(doc.strip()) exit() if version and any(o.name == "--version" and o.value for o in options): print(version) exit() class Dict(dict): def __repr__(self): return "{%s}" % ",\n ".join("%r: %r" % i for i in sorted(self.items())) def docopt(doc, argv=sys.argv[1:], help=True, version=None): DocoptExit.usage = docopt.usage = usage = printable_usage(doc) pot_options = parse_doc_options(doc) formal_pattern = parse_pattern(formal_usage(usage), options=pot_options) argv = parse_args(argv, options=pot_options) extras(help, version, argv, doc) matched, left, arguments = formal_pattern.fix().match(argv) if matched and left == []: # better message if left? options = [o for o in argv if type(o) is Option] pot_arguments = [ a for a in formal_pattern.flat if type(a) in [Argument, Command] ] return Dict( (a.name, a.value) for a in (pot_options + options + pot_arguments + arguments) ) raise DocoptExit() port-for-0.6.1/src/port_for/ephemeral.py000066400000000000000000000037161405543004300202300ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ This module provide utilities to find ephemeral port ranges for the current OS. See http://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html for more info about ephemeral port ranges. Currently only Linux and BSD (including OS X) are supported. """ import subprocess from typing import List, Tuple, Dict DEFAULT_EPHEMERAL_PORT_RANGE = (32768, 65535) def port_ranges() -> List[Tuple[int, int]]: """ Returns a list of ephemeral port ranges for current machine. """ try: return _linux_ranges() except (OSError, IOError): # not linux, try BSD try: ranges = _bsd_ranges() if ranges: return ranges except (OSError, IOError): pass # fallback return [DEFAULT_EPHEMERAL_PORT_RANGE] def _linux_ranges() -> List[Tuple[int, int]]: with open("/proc/sys/net/ipv4/ip_local_port_range") as f: # use readline() instead of read() for linux + musl low, high = f.readline().split() return [(int(low), int(high))] def _bsd_ranges() -> List[Tuple[int, int]]: pp = subprocess.Popen( ["sysctl", "net.inet.ip.portrange"], stdout=subprocess.PIPE ) stdout, stderr = pp.communicate() lines = stdout.decode("ascii").split("\n") out: Dict[str, str] = dict( [ [ x.strip().rsplit(".")[-1] # type: ignore[misc] for x in line.split(":") ] for line in lines if line ] ) ranges = [ # FreeBSD & Mac ("first", "last"), ("lowfirst", "lowlast"), ("hifirst", "hilast"), # OpenBSD ("portfirst", "portlast"), ("porthifirst", "porthilast"), ] res = [] for rng in ranges: try: low, high = int(out[rng[0]]), int(out[rng[1]]) if low <= high: res.append((low, high)) except KeyError: pass return res port-for-0.6.1/src/port_for/exceptions.py000066400000000000000000000001041405543004300204330ustar00rootroot00000000000000# -*- coding: utf-8 -*- class PortForException(Exception): pass port-for-0.6.1/src/port_for/py.typed000066400000000000000000000000001405543004300173720ustar00rootroot00000000000000port-for-0.6.1/src/port_for/store.py000066400000000000000000000053321405543004300174160ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os from configparser import ConfigParser, DEFAULTSECT from typing import Optional, List, Tuple, Union from .api import select_random from .exceptions import PortForException DEFAULT_CONFIG_PATH = "/etc/port-for.conf" class PortStore(object): def __init__(self, config_filename: str = DEFAULT_CONFIG_PATH): self._config = config_filename def bind_port( self, app: str, port: Optional[Union[int, str]] = None ) -> int: if "=" in app or ":" in app: raise Exception('invalid app name: "%s"' % app) requested_port: Optional[str] = None if port is not None: requested_port = str(port) parser = self._get_parser() # this app already use some port; return it if parser.has_option(DEFAULTSECT, app): actual_port = parser.get(DEFAULTSECT, app) if requested_port is not None and requested_port != actual_port: msg = ( "Can't bind to port %s: %s is already associated " "with port %s" % (requested_port, app, actual_port) ) raise PortForException(msg) return int(actual_port) # port is already used by an another app app_by_port = dict((v, k) for k, v in parser.items(DEFAULTSECT)) bound_port_numbers = map(int, app_by_port.keys()) if requested_port is None: requested_port = str( select_random(exclude_ports=bound_port_numbers) ) if requested_port in app_by_port: binding_app = app_by_port[requested_port] if binding_app != app: raise PortForException( "Port %s is already used by %s!" % (requested_port, binding_app) ) # new app & new port parser.set(DEFAULTSECT, app, requested_port) self._save(parser) return int(requested_port) def unbind_port(self, app: str) -> None: parser = self._get_parser() parser.remove_option(DEFAULTSECT, app) self._save(parser) def bound_ports(self) -> List[Tuple[str, int]]: return [ (app, int(port)) for app, port in self._get_parser().items(DEFAULTSECT) ] def _ensure_config_exists(self) -> None: if not os.path.exists(self._config): with open(self._config, "wb"): pass def _get_parser(self) -> ConfigParser: self._ensure_config_exists() parser = ConfigParser() parser.read(self._config) return parser def _save(self, parser: ConfigParser) -> None: with open(self._config, "wt") as f: parser.write(f) port-for-0.6.1/src/port_for/utils.py000066400000000000000000000013421405543004300174170ustar00rootroot00000000000000# -*- coding: utf-8 -*- import itertools from typing import Iterable, Iterator, Tuple, Set def ranges_to_set(lst: Iterable[Tuple[int, int]]) -> Set[int]: """ Convert a list of ranges to a set of numbers:: >>> ranges = [(1,3), (5,6)] >>> sorted(list(ranges_to_set(ranges))) [1, 2, 3, 5, 6] """ return set(itertools.chain(*(range(x[0], x[1] + 1) for x in lst))) def to_ranges(lst: Iterable[int]) -> Iterator[Tuple[int, int]]: """ Convert a list of numbers to a list of ranges:: >>> numbers = [1,2,3,5,6] >>> list(to_ranges(numbers)) [(1, 3), (5, 6)] """ for a, b in itertools.groupby(enumerate(lst), lambda t: t[1] - t[0]): c = list(b) yield c[0][1], c[-1][1] port-for-0.6.1/tests/000077500000000000000000000000001405543004300144265ustar00rootroot00000000000000port-for-0.6.1/tests/test_cases.py000066400000000000000000000117471405543004300171470ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest import tempfile import mock import socket import os from typing import Union, List, Set, Tuple import pytest import port_for from port_for.api import get_port from port_for.utils import ranges_to_set def test_common_ports() -> None: assert not port_for.is_available(80) assert not port_for.is_available(11211) def test_good_port_ranges() -> None: ranges = [ (10, 15), # too short (100, 200), # good (220, 245), # a bit short (300, 330), # good (440, 495), # also good ] ports = ranges_to_set(ranges) good_ranges = port_for.good_port_ranges(ports, 20, 3) assert good_ranges == [(103, 197), (443, 492), (303, 327)], good_ranges def test_something_works() -> None: assert len(port_for.good_port_ranges()) > 10 assert len(port_for.available_good_ports()) > 1000 def test_binding() -> None: # low ports are not available assert port_for.port_is_used(10) def test_binding_high() -> None: s = socket.socket() s.bind(("", 0)) port = s.getsockname()[1] assert port_for.port_is_used(port) s.close() assert not port_for.port_is_used(port) def test_get_port_random() -> None: """Test case allowing get port to randomly select any port.""" assert get_port(None) def test_get_port_none() -> None: """Test special case for get_port to return None.""" assert not get_port(-1) @pytest.mark.parametrize("port", (1234, "1234")) def test_get_port_specific(port: Union[str, int]) -> None: """Test special case for get_port to return same value.""" assert get_port(port) == 1234 @pytest.mark.parametrize( "port_range", ( [(2000, 3000)], (2000, 3000), ), ) def test_get_port_from_range( port_range: Union[List[Tuple[int, int]], Tuple[int, int]] ) -> None: """Test getting random port from given range.""" assert get_port(port_range) in list(range(2000, 3000 + 1)) @pytest.mark.parametrize( "port_set", ( [{4001, 4002, 4003}], {4001, 4002, 4003}, ), ) def test_get_port_from_set(port_set: Union[List[Set[int]], Set[int]]) -> None: """Test getting random port from given set.""" assert get_port(port_set) in {4001, 4002, 4003} def test_port_mix() -> None: """Test getting random port from given set and range.""" sets_and_ranges: List[Union[Tuple[int, int], Set[int]]] = [ (2000, 3000), {4001, 4002, 4003}, ] assert get_port(sets_and_ranges) in set(range(2000, 3000 + 1)) and { 4001, 4002, 4003, } class SelectPortTest(unittest.TestCase): @mock.patch("port_for.api.port_is_used") def test_all_used(self, port_is_used: mock.MagicMock) -> None: port_is_used.return_value = True self.assertRaises(port_for.PortForException, port_for.select_random) @mock.patch("port_for.api.port_is_used") def test_random_port(self, port_is_used: mock.MagicMock) -> None: ports = set([1, 2, 3]) used = {1: True, 2: False, 3: True} port_is_used.side_effect = lambda port: used[port] for x in range(100): self.assertEqual(port_for.select_random(ports), 2) class StoreTest(unittest.TestCase): def setUp(self) -> None: fd, self.fname = tempfile.mkstemp() self.store = port_for.PortStore(self.fname) def tearDown(self) -> None: os.remove(self.fname) def test_store(self) -> None: assert self.store.bound_ports() == [] port = self.store.bind_port("foo") self.assertTrue(port) self.assertEqual(self.store.bound_ports(), [("foo", port)]) self.assertEqual(port, self.store.bind_port("foo")) port2 = self.store.bind_port("aar") self.assertNotEqual(port, port2) self.assertEqual( self.store.bound_ports(), [("foo", port), ("aar", port2)] ) self.store.unbind_port("aar") self.assertEqual(self.store.bound_ports(), [("foo", port)]) def test_rebind(self) -> None: # try to rebind an used port for an another app port = self.store.bind_port("foo") self.assertRaises( port_for.PortForException, self.store.bind_port, "baz", port ) def test_change_port(self) -> None: # changing app ports is not supported. port = self.store.bind_port("foo") another_port = port_for.select_random() assert port != another_port self.assertRaises( port_for.PortForException, self.store.bind_port, "foo", another_port ) def test_bind_unavailable(self) -> None: # it is possible to explicitly bind currently unavailable port port = self.store.bind_port("foo", 80) self.assertEqual(port, 80) self.assertEqual(self.store.bound_ports(), [("foo", 80)]) def test_bind_non_auto(self) -> None: # it is possible to pass a port port = port_for.select_random() res_port = self.store.bind_port("foo", port) self.assertEqual(res_port, port)