[
  {
    "path": ".appveyor.yml",
    "content": "# adapted from https://packaging.python.org/en/latest/appveyor/\n\n\nenvironment:\n  # we tell Tox to use \"twisted[windows]\", to get pypiwin32 installed\n  #TWISTED_EXTRAS: \"[windows]\"\n  # that didn't work (it seems to work when I run it locally, but on appveyor\n  # it fails to install the pypiwin32 package). So don't bother telling\n  # Twisted to support windows: just install it ourselves.\n  # EXTRA_DEPENDENCY: \"pypiwin32\"\n  matrix:\n    # For Python versions available on Appveyor, see\n    # http://www.appveyor.com/docs/installed-software#python\n    - PYTHON: \"C:\\\\Python27\"\n    - PYTHON: \"C:\\\\Python27-x64\"\n      DISTUTILS_USE_SDK: \"1\"\n    - PYTHON: \"C:\\\\Python35\"\n    - PYTHON: \"C:\\\\Python36\"\n    - PYTHON: \"C:\\\\Python36-x64\"\n\ninstall:\n  - |\n    %PYTHON%\\python.exe -m pip install wheel tox\n\n# note:\n# %PYTHON% has: python.exe\n# %PYTHON%\\Scripts has: pip.exe, tox.exe (and others installed by bare pip)\n\n\nbuild: off\n\ntest_script:\n  # Put your test command here.\n  # Note that you must use the environment variable %PYTHON% to refer to\n  # the interpreter you're using - Appveyor does not do anything special\n  # to put the Python evrsion you want to use on PATH.\n  - |\n    misc\\windows-build.cmd %PYTHON%\\Scripts\\tox.exe -e py\n\nafter_test:\n  # This step builds your wheels.\n  # Again, you only need build.cmd if you're building C extensions for\n  # 64-bit Python 3.3/3.4. And you need to use %PYTHON% to get the correct\n  # interpreter\n  - |\n    misc\\windows-build.cmd %PYTHON%\\python.exe setup.py bdist_wheel\n\nartifacts:\n  # bdist_wheel puts your built wheel in the dist directory\n  - path: dist\\*\n\n#on_success:\n#  You can use this step to upload your artifacts to a public website.\n#  See Appveyor's documentation for more details. Or you can simply\n#  access your wheels from the Appveyor \"artifacts\" tab for your build.\n"
  },
  {
    "path": ".coveragerc",
    "content": "# -*- mode: conf -*-\n\n[run]\n# only record trace data for wormhole_mailbox_server.*\nsource =\n   wormhole_mailbox_server\n# and don't trace the test files themselves, or Versioneer's stuff\nomit =\n   src/wormhole_mailbox_server/test/*\n   src/wormhole_mailbox_server/_version.py\n\n\n# This allows 'coverage combine' to correlate the tracing data built while\n# running tests in multiple tox virtualenvs. To take advantage of this\n# properly, use \"coverage erase\" before tox, \"coverage run --parallel-mode\"\n# inside tox to avoid overwriting the output data (by writing it into\n# .coverage-XYZ instead of just .coverage), and run \"coverage combine\"\n# afterwards.\n\n[paths]\nsource =\n       src/\n       .tox/*/lib/python*/site-packages/\n       .tox/pypy*/site-packages/\n"
  },
  {
    "path": ".gitattributes",
    "content": "src/wormhole_mailbox_server/_version.py export-subst\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  testing:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n\n    steps:\n    - uses: actions/checkout@v2\n\n    - name: Set up Python\n      uses: actions/setup-python@v2\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip tox codecov\n        tox --notest -e coverage\n\n    - name: Test\n      run: |\n        python --version\n        tox -e coverage\n\n    - name: Upload Coverage\n      run: codecov\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n.eggs\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.cache\nnosetests.xml\ncoverage.xml\n_trial_temp/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n/twistd.pid\n/relay.sqlite\n/usage.sqlite\n\n# Virtual environment stuff\nvenv/\n/src/twisted/plugins/dropin.cache\n"
  },
  {
    "path": ".travis.yml",
    "content": "arch:\n  - amd64\n  - ppc64le\nlanguage: python\n# defaults: the py3.7 environment overrides these\ndist: trusty\nsudo: false\n\ncache: pip\nbefore_cache:\n  - rm -f $HOME/.cache/pip/log/debug.log\nbranches:\n  except:\n    - /^WIP-.*$/\ninstall:\n  - pip install -U pip tox virtualenv codecov\nscript:\n  - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then\n      tox -e flake8 ;\n    fi\n  - tox -e coverage\nafter_success:\n  - codecov\nmatrix:\n  include:\n    - python: 2.7\n    - python: 3.5\n    - python: 3.6\n    - python: 3.7\n      # we don't actually need sudo, but that kicks us onto GCE, which lets\n      # us get xenial\n      sudo: true\n      dist: xenial\n    - python: nightly\n  allow_failures:\n    - python: nightly\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Brian Warner\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include versioneer.py\ninclude src/wormhole_mailbox_server/_version.py\ninclude LICENSE README.md NEWS.md\nrecursive-include docs *.md *.rst *.dot\ninclude docs/conf.py docs/Makefile\ninclude .coveragerc tox.ini snapcraft.yaml\ninclude misc/windows-build.cmd\ninclude misc/*.py\ninclude misc/munin/wormhole_active\ninclude misc/munin/wormhole_errors\ninclude misc/munin/wormhole_event_rate\ninclude misc/munin/wormhole_events\ninclude misc/munin/wormhole_events_alltime\n"
  },
  {
    "path": "Makefile",
    "content": "# How to Make a Release\n# ---------------------\n#\n# This file answers the question \"how to make a release\" hopefully\n# better than a document does (only meejah and warner may currently do\n# the \"upload to PyPI\" part anyway)\n#\n\ndefault:\n\techo \"see Makefile\"\n\nrelease-clean:\n\t@echo \"Cleanup stale release: \" `python newest-version.py`\n\t-rm NEWS.md.asc\n\t-rm dist/magic_wormhole_mailbox_server-`python newest-version.py`.tar.gz*\n\t-rm dist/magic_wormhole_mailbox_server-`python newest-version.py`-py3-none-any.whl*\n\tgit tag -d `python newest-version.py`\n\n# create a branch, like: git checkout -b prepare-release-0.16.0\n# then run these, so CI can run on the release\nrelease:\n\t@echo \"Is checkout clean?\"\n\tgit diff-files --quiet\n\tgit diff-index --quiet --cached HEAD --\n\n\t@echo \"Install required build software\"\n\tpython -m pip install --editable .[dev,release]\n\n\t@echo \"Test README\"\n\tpython setup.py check -s\n\n\t@echo \"Is GPG Agent rubnning, and has key?\"\n\tgpg --pinentry=loopback -u meejah@meejah.ca --armor --clear-sign NEWS.md\n\n\t@echo \"Bump version and create tag\"\n\tpython update-version.py\n#\tpython update-version.py --patch  # for bugfix release\n\n\t@echo \"Build and sign wheel\"\n\tpython setup.py bdist_wheel\n\tgpg --pinentry=loopback -u meejah@meejah.ca --armor --detach-sign dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`-py3-none-any.whl\n\tls dist/*`git describe --abbrev=0`*\n\n\t@echo \"Build and sign source-dist\"\n\tpython setup.py sdist\n\tgpg --pinentry=loopback -u meejah@meejah.ca --armor --detach-sign dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`.tar.gz\n\tls dist/*`git describe --abbrev=0`*\n\nrelease-test:\n\tgpg --verify dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`.tar.gz.asc\n\tgpg --verify dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`-py3-none-any.whl.asc\n\tpython -m venv testmf_venv\n\ttestmf_venv/bin/pip install --upgrade pip\n\ttestmf_venv/bin/pip install dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`-py3-none-any.whl\n\ttestmf_venv/bin/twistd wormhole-mailbox --version\n\ttestmf_venv/bin/pip uninstall -y magic_wormhole_mailbox_server\n\ttestmf_venv/bin/pip install dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`.tar.gz\n\ttestmf_venv/bin/twistd wormhole-mailbox --version\n\trm -rf testmf_venv\n\nrelease-upload:\n\ttwine upload --username __token__ --password `cat PRIVATE-release-token` dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`-py3-none-any.whl dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`-py3-none-any.whl.asc dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`.tar.gz dist/magic_wormhole_mailbox_server-`git describe --abbrev=0`.tar.gz.asc\n\tmv dist/*-`git describe --abbrev=0`.tar.gz.asc signatures/\n\tmv dist/*-`git describe --abbrev=0`-py3-none-any.whl.asc signatures/\n\tgit add signatures/magic_wormhole_mailbox_server-`git describe --abbrev=0`.tar.gz.asc\n\tgit add signatures/magic_wormhole_mailbox_server-`git describe --abbrev=0`-py3-none-any.whl.asc\n\tgit commit -m \"signatures for release\"\n\tgit push origin-push `git describe --abbrev=0`\n\n\ndilation.png: dilation.seqdiag\n\tseqdiag --no-transparency -T png --size 1000x800 -o dilation.png\n"
  },
  {
    "path": "NEWS.md",
    "content": "\n\nUser-visible changes in \"magic-wormhole-mailbox-server\":\n\n## Upcoming\n\n* (put release-notes here when merging / proposing a PR)\n\n\n## Release 0.8.0 (15-May-2026)\n\n* Server header properly reports version (#27)\n* introduce a ``\"your-address\"`` key to the ``\"welcome\"`` message (to reflect the IP address and port back, #63)\n\n\n## Release 0.7.0 (14-May-2026)\n\n* CI no longer tests Python 3.9 @meejah\n* CI now tests 3.14 @meejah\n* non-numeric nameplates rejected with error (@meejah)\n* more-complete sequence and state diagrams (@meejah)\n* update Munin plugin shebang (@warner)\n* Munin plugins open db read-only (@warner)\n\n\n## Release 0.6.0 (13-Feb-2026)\n\n* CI no longer tests Python 3.8 (it is EOL)\n* add Python 3.14\n* fix link to transit-relay (@nirit100)\n* fix stdout test error (@sblondon)\n* remove depracated pkg_resources use (@sblondon)\n* syntax modernization (@sblondon)\n* use f-strings everywhere (@sblondon)\n* replace returnValue() with return (@p12tic)\n* make README match tested versions (@p12tic)\n* no need to install mock (@bkmgit)\n\n\n## Release 0.5.1 (9-Nov-2024)\n\n* properly require \"setuptools\" for install (#47, jameshilliard)\n\n\n## Release 0.5.0 (7-Nov-2024)\n\n* correctly close a mailbox which still has a nameplate (#28)\n* remove python2 support\n* test on python 3.8, 3.9, 3.10, 3.11 and 3.12 series\n* drop \"six\" (#35)\n* upgrade \"versioneer\"\n\n\n## Release 0.4.1 (11-Sep-2019)\n\n* listen on IPv4+IPv6 properly (#16)\n\n\n## Release 0.4.0 (10-Sep-2019)\n\n* listen on IPv4+IPv6 socket by default (#16)\n* deallocate AppNamespace objects when empty (#12)\n* add client-version-uptake munin plugin\n* drop support for py3.3 and py3.4\n\n\n## Release 0.3.1 (23-Jun-2018)\n\nRecord 'None' for when client doesn't supply a version, to make the math\neasier.\n\n\n## Release 0.3.0 (23-Jun-2018)\n\nFix munin plugins, record client versions in usageDB.\n\n\n## Release 0.2.0 (16-Jun-2018)\n\nImprove install docs, clean up Munin plugins, add DB migration tool.\n\n\n## Release 0.1.0 (19-Feb-2018)\n\nInitial release: Forked from magic-wormhole-0.10.5 (14-Feb-2018)\n"
  },
  {
    "path": "README.md",
    "content": "# Magic Wormhole Mailbox Server\n[![PyPI](http://img.shields.io/pypi/v/magic-wormhole-mailbox-server.svg)](https://pypi.python.org/pypi/magic-wormhole-mailbox-server)\n![Tests](https://github.com/magic-wormhole/magic-wormhole-transit-relay/workflows/Tests/badge.svg)\n[![codecov.io](https://codecov.io/github/magic-wormhole/magic-wormhole-transit-relay/coverage.svg?branch=master)](https://codecov.io/github/magic-wormhole/magic-wormhole-transit-relay?branch=master)\n\nThis repository holds the code for the main server that\n[Magic-Wormhole](http://magic-wormhole.io) clients connect to. The server\nperforms store-and-forward delivery for small key-exchange and control\nmessages. Bulk data is sent over a direct TCP connection, or through a\n[transit-relay](https://github.com/magic-wormhole/magic-wormhole-transit-relay).\n\nClients connect with WebSockets, for low-latency delivery in the happy case\nwhere both clients are attached at the same time. Message are stored to\nenable non-simultaneous clients to make forward progress. The server uses a\nsmall SQLite database for persistence (and clients will reconnect\nautomatically, allowing the server to be rebooted without losing state). An\noptional \"usage DB\" tracks historical activity for status monitoring and\noperational maintenance.\n\n## Installation\n\n```\npip install magic-wormhole-mailbox-server\n```\n\nYou either want to do this into a \"user\" environment (putting the ``twist``\nand ``twistd`` executables in ``~/.local/bin/``) like this:\n\n```\npip install --user magic-wormhole-mailbox-server\n```\n\nor put it into a virtualenv, to avoid modifying the system python's\nlibraries, like this:\n\n```\nvirtualenv venv\nsource venv/bin/activate\npip install magic-wormhole-mailbox-server\n```\n\nYou probably *don't* want to use ``sudo`` when you run ``pip``, since the\ndependencies that get installed may conflict with other python programs on\nyour computer. ``pipsi`` is usually a good way to install into isolated\nenvironments, but unfortunately it doesn't work for\nmagic-wormhole-mailbox-server, because we don't have a dedicated command to\nstart the server (``twist``, described below, comes from the ``twisted``\npackage, and pipsi doesn't expose executables from dependencies).\n\nFor the installation from source, ``clone`` this repo, ``cd`` into the folder,\ncreate and activate a virtualenv, and run ``pip install .``.\n\n## Running A Server\n\nNote that the standard [Magic-Wormhole](http://magic-wormhole.io)\ncommand-line tool is preconfigured to use a mailbox server hosted by the\nproject, so running your own server is only necessary for custom applications\nthat use magic-wormhole as a library.\n\nThe mailbox server is deployed as a twist/twistd plugin. Running a basic\nserver looks like this:\n\n```\ntwist wormhole-mailbox --usage-db=usage.sqlite\n```\n\nUse ``twist wormhole-mailbox --help`` for more details.\n\nIf you use the default ``--port=tcp:4000``, on a machine named\n``example.com``, then clients can reach your server with the following\noption:\n\n```\nwormhole --relay-url=ws://example.com:4000/v1 send FILENAME\n```\n\n## Using Docker\n\nDockerfile content:\n```dockerfile\nFROM python:3.11\nRUN pip install magic-wormhole-mailbox-server\nCMD [ \"twist\", \"wormhole-mailbox\",\"--usage-db=usage.sqlite\" ]\n```\n> Note: This will be running as root, you should adjust it to be in user space for production.\n\nBuild and run:\n```shell\ndocker build -t magicwormhole Dockerfile\ndocker run -p 4000:4000 -d magicwormhole\n```\n\nConnect:\n```shell\nwormhole --relay-url=ws://localhost:4000/v1 send FILENAME\n```\n\n## License, Compatibility\n\nThis library is released under the MIT license, see LICENSE for details.\n\nThis library is compatible with python3 (3.10 and higher).\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = Magic-Wormhole-Mailbox-Server\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile diagrams\n\ndiagrams:\n\tplantuml -Tpng happy-plant.seq\n\tseqdiag --no-transparency --antialias -T png happy.seq\n\tdot -Tpng states.dot > states.png\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Magic-Wormhole documentation build configuration file, created by\n# sphinx-quickstart on Sun Nov 12 10:24:09 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\nfrom recommonmark.parser import CommonMarkParser\n\nsource_parsers = {\n    \".md\": CommonMarkParser,\n}\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = []\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\nsource_suffix = ['.rst', '.md']\n#source_suffix = '.md'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = 'Magic-Wormhole-Mailbox-Server'\ncopyright = '2018, Brian Warner'\nauthor = 'Brian Warner'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\ndef _get_versions():\n    import os.path, sys, subprocess\n    here = os.path.dirname(os.path.abspath(__file__))\n    parent = os.path.dirname(here)\n    v = subprocess.check_output([sys.executable, \"setup.py\", \"--version\"],\n                                cwd=parent)\n    v = v.decode(\"ascii\")\n    short = \".\".join(v.split(\".\")[:2])\n    long = v\n    return short, long\nversion, release = _get_versions()\n# The short X.Y version.\n#version = u'0.10'\n# The full version, including alpha/beta/rc tags.\n#release = u'0.10.3'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = None\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'alabaster'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Custom sidebar templates, must be a dictionary that maps document names\n# to template names.\n#\n# This is required for the alabaster theme\n# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars\nhtml_sidebars = {\n    '**': [\n        'relations.html',  # needs 'show_related': True theme option to display\n        'searchbox.html',\n    ]\n}\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'Magic-Wormhole-Mailbox-Serverdoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'Magic-Wormhole-Mailbox-Server.tex', 'Magic-Wormhole-Mailbox-Server Documentation',\n     'Brian Warner', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'magic-wormhole-mailbox-server', 'Magic-Wormhole-Mailbox-Server Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'Magic-Wormhole-Mailbox-Server',\n     'Magic-Wormhole-Mailbox-Server Documentation',\n     author, 'Magic-Wormhole-Mailbox-Server',\n     'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n"
  },
  {
    "path": "docs/happy-plant.seq",
    "content": "@startuml\n\nskinparam defaultFontName \"Source Sans Pro\"\nskinparam defaultFontSize 18\nskinparam participantFontSize 22\nskinparam arrowFontSize 18\nskinparam noteFontSize 22\n\ntitle Magic Folder Server\n\n  alice <- mailbox: welcome\n  alice -> mailbox: bind appid, side\n\n  alice -> mailbox: allocate\n  alice <- mailbox: allocated: nameplate_id\n\nactivate alice #aaddff\n\n  alice -> mailbox: claim nameplate_id\n  alice <- mailbox: claimed: mailbox_id\n  alice -> mailbox: open mailbox_id\n\n  alice -> mailbox: add phase=pake body\n  alice <- mailbox: message phase=pake side=a body\n\n... code communicated out-of-band ...\n\n  bob <- mailbox: welcome\n  bob -> mailbox: bind appid, side\n  bob -> mailbox: claim nameplate_id\n  bob <- mailbox: claimed: mailbox_id\n  bob -> mailbox: open mailbox_id\n  bob <- mailbox: message phase=pake side=a body\n  bob -> mailbox: add phase=pake body\n  bob <- mailbox: mesage phase=pake side=b body\n\n  alice <- mailbox: mesage phase=pake side=b body\n  alice -> mailbox: release nameplate_id\n  alice <- mailbox: released\n\ndeactivate alice\n\nnote over mailbox #eeeeee: Key-Verification messages double as version exchange\n\n  alice -> mailbox: add phase=version side=a body\n\n  bob <- mailbox: message phase=version side=a body\n  alice <- mailbox: message phase=version side=a body\n\n  bob -> mailbox: add phase=version side=b body\n  bob <- mailbox: message phase=version side=b body\nactivate bob #lightgreen\n  alice <- mailbox: message phase=version side=b body\n\nactivate alice #lightgreen\n\n  alice -> mailbox: add phase=0 side=a body\n  alice <- mailbox: message phase=0 side=a body\n  bob   <- mailbox: message phase=0 side=a body\n\n' XXX does this help or hinter understanding?\n  alice -->> bob: (effectively msg 0 from alice->bob)\n\n  alice -> mailbox: add phase=1 side=a body\n  alice <- mailbox: message phase=1 side=a body\n  bob   <- mailbox: message phase=1 side=a body\n\n' XXX does this help or hinter understanding?\n  alice -->> bob: (effectively msg 1 from alice->bob)\n\n  bob -> mailbox: add phase=0 side=b body\n  bob   <- mailbox: message phase=0 side=b body\n  alice <- mailbox: message phase=0 side=b body\n\n' XXX does this help or hinter understanding?\n  alice <<-- bob: (effectively msg 0 from bob->alice)\n\ndeactivate alice\ndeactivate bob\n\n' group #Pink Closing\ngroup Closing\n\n  bob -> mailbox: close mailbox_id mood\n  bob <- mailbox: closed\n\n  alice -> mailbox: close mailbox_id mood\n  alice <- mailbox: closed\n\nend\n\n@enduml"
  },
  {
    "path": "docs/happy.seq",
    "content": "seqdiag {\n  alice <- mailbox [label = \"welcome\"]\n  alice -> mailbox [label = \"bind appid, side\"]\n  alice -> mailbox [label = \"allocate\", note = \"side assigned\"]\n  alice <- mailbox [label = \"allocated: nameplate_id\"]\n  alice -> mailbox [label = \"claim nameplate_id\"]\n  alice <- mailbox [label = \"claimed: mailbox_id\"]\n  alice -> mailbox [label = \"open mailbox_id\"]\n\n  alice -> mailbox [label = \"add phase=pake body\"]\n  alice <- mailbox [label = \"message phase=pake side=a body\"]\n\n  bob <- mailbox [label = \"welcome\"]\n  bob -> mailbox [label = \"bind appid, side\"]\n// note: no allocate / allocated\n  bob -> mailbox [label = \"claim nameplate_id\"]\n  bob <- mailbox [label = \"claimed: mailbox_id\"]\n  bob -> mailbox [label = \"open mailbox_id\"]\n  bob <- mailbox [label = \"message phase=pake side=a body\"]\n  bob -> mailbox [label = \"add phase=pake body\"]\n  bob <- mailbox [label = \"mesage phase=pake side=b body\"]\n\n  alice <- mailbox [label = \"mesage phase=pake side=b body\"]\n  alice -> mailbox [label = \"release nameplate_id\"]\n  alice <- mailbox [label = \"released\"]\n  alice -> mailbox [label = \"add phase=version side=a body\"]\n\n  bob <- mailbox [label = \"message phase=version side=a body\"]\n  alice <- mailbox [label = \"message phase=version side=a body\"]\n\n  bob -> mailbox [label = \"add phase=version side=b body\"]\n  bob <- mailbox [label = \"message phase=version side=b body\"]\n  alice <- mailbox [label = \"message phase=version side=b body\"]\n\n=== application messages ===\n\n  alice -> mailbox [label = \"add phase=0 side=a body\"]\n  alice <- mailbox [label = \"message phase=0 side=a body\"]\n  bob   <- mailbox [label = \"message phase=0 side=a body\"]\n\n  alice -> mailbox [label = \"add phase=1 side=a body\"]\n  alice <- mailbox [label = \"message phase=1 side=a body\"]\n  bob   <- mailbox [label = \"message phase=1 side=a body\"]\n\n  bob -> mailbox [label = \"add phase=0 side=b body\"]\n  bob   <- mailbox [label = \"message phase=0 side=b body\"]\n  alice <- mailbox [label = \"message phase=0 side=b body\"]\n\n=== closing ===\n\n  bob -> mailbox [label = \"close mailbox_id mood\"]\n  bob <- mailbox [label = \"closed\"]\n\n  alice -> mailbox [label = \"close mailbox_id mood\"]\n  alice <- mailbox [label = \"closed\"]\n\n}\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. Magic-Wormhole-Mailbox-Server documentation master file, created by\n   sphinx-quickstart on Sun Nov 12 10:24:09 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nMagic-Wormhole-Mailbox-Server: backend server for magic-wormhole\n================================================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n   welcome\n\n   server-protocol\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/server-protocol.md",
    "content": "# Rendezvous Server Protocol\n\n## Concepts\n\nThe Rendezvous Server provides queued delivery of binary messages from one\nclient to a second, and vice versa. Each message contains a \"phase\" (a\nstring) and a body (bytestring). These messages are queued in a \"Mailbox\"\nuntil the other side connects and retrieves them, but are delivered\nimmediately if both sides are connected to the server at the same time.\n\nMailboxes are identified by a large random string. \"Nameplates\", in contrast,\nhave short numeric identities: in a wormhole code like \"4-purple-sausages\",\nthe \"4\" is the nameplate.\n\nEach client has a randomly-generated \"side\", a short hex string, used to\ndifferentiate between echoes of a client's own message, and real messages\nfrom the other client.\n\n## Application IDs\n\nThe server isolates each application from the others. Each client provides an\n\"App Id\" when it first connects (via the \"BIND\" message), and all subsequent\ncommands are scoped to this application. This means that nameplates\n(described below) and mailboxes can be re-used between different apps. The\nAppID is a unicode string. Both sides of the wormhole must use the same\nAppID, of course, or they'll never see each other. The server keeps track of\nwhich applications are in use for maintenance purposes.\n\nEach application should use a unique AppID. Developers are encouraged to use\n\"DNSNAME/APPNAME\" to obtain a unique one: e.g. the `bin/wormhole`\nfile-transfer tool uses `lothar.com/wormhole/text-or-file-xfer`.\n\n## WebSocket Transport\n\nAt the lowest level, each client establishes (and maintains) a WebSocket\nconnection to the Rendezvous Server. If the connection is lost (which could\nhappen because the server was rebooted for maintenance, or because the\nclient's network connection migrated from one network to another, or because\nthe resident network gremlins decided to mess with you today), clients should\nreconnect after waiting a random (and exponentially-growing) delay. The\nPython implementation waits about 1 second after the first connection loss,\ngrowing by 50% each time, capped at 1 minute.\n\nEach message to the server is a dictionary, with at least a `type` key, and\nother keys that depend upon the particular message type. Messages from server\nto client follow the same format.\n\n`misc/dump-timing.py` is a debug tool which renders timing data gathered from\nthe server and both clients, to identify protocol slowdowns and guide\noptimization efforts. To support this, the client/server messages include\nadditional keys. Client->Server messages include a random `id` key, which is\ncopied into the `ack` that is immediately sent back to the client for all\ncommands (logged for the timing tool but otherwise ignored). Some\nclient->server messages (`list`, `allocate`, `claim`, `release`, `close`,\n`ping`) provoke a direct response by the server: for these, `id` is copied\ninto the response. This helps the tool correlate the command and response.\nAll server->client messages have a `server_tx` timestamp (seconds since\nepoch, as a float), which records when the message left the server. Direct\nresponses include a `server_rx` timestamp, to record when the client's\ncommand was received. The tool combines these with local timestamps (recorded\nby the client and not shared with the server) to build a full picture of\nnetwork delays and round-trip times.\n\nAll messages are serialized as JSON, encoded to UTF-8, and the resulting\nbytes sent as a single \"binary-mode\" WebSocket payload.\n\nServers can signal `error` for any message type it does not recognize.\nClients and Servers must ignore unrecognized keys in otherwise-recognized\nmessages. Clients must ignore unrecognized message types from the Server.\n\n## Connection-Specific (Client-to-Server) Messages\n\nThe first thing each client sends to the server, immediately after the\nWebSocket connection is established, is a `bind` message. This specifies the\nAppID and side (in keys `appid` and `side`, respectively) that all subsequent\nmessages will be scoped to. While technically each message could be\nindependent (with its own `appid` and `side`), I thought it would be less\nconfusing to use exactly one WebSocket per logical wormhole connection.\n\nThe first thing the server sends to each client is the `welcome` message.\nThis is intended to deliver important status information to the client that\nmight influence its operation. The Python client currently reacts to the\nfollowing keys (and ignores all others):\n\n* `current_cli_version`: prompts the user to upgrade if the server's\n  advertised version is greater than the client's version (as derived from\n  the git tag)\n* `motd`: prints this message, if present; intended to inform users about\n  performance problems, scheduled downtime, or to beg for donations to keep\n  the server running\n* `error`: causes the client to print the message and then terminate. If a\n  future version of the protocol requires a rate-limiting CAPTCHA ticket or\n  other authorization record, the server can send `error` (explaining the\n  requirement) if it does not see this ticket arrive before the `bind`.\n\nA `ping` will provoke a `pong`: these are only used by unit tests for\nsynchronization purposes (to detect when a batch of messages have been fully\nprocessed by the server). NAT-binding refresh messages are handled by the\nWebSocket layer (by asking Autobahn to send a keepalive messages every 60\nseconds), and do not use `ping`.\n\nIf any client->server command is invalid (e.g. it lacks a necessary key, or\nwas sent in the wrong order), an `error` response will be sent, This response\nwill include the error string in the `error` key, and a full copy of the\noriginal message dictionary in `orig`.\n\n## Nameplates\n\nWormhole codes look like `4-purple-sausages`, consisting of a number followed\nby some random words. This number is called a \"Nameplate\".\n\nOn the Rendezvous Server, the Nameplate contains a pointer to a Mailbox.\nClients can \"claim\" a nameplate, and then later \"release\" it. Each claim is\nfor a specific side (so one client claiming the same nameplate multiple times\nonly counts as one claim). Nameplates are deleted once the last client has\nreleased it, or after some period of inactivity.\n\nClients can either make up nameplates themselves, or (more commonly) ask the\nserver to allocate one for them. Allocating a nameplate automatically claims\nit (to avoid a race condition), but for simplicity, clients send a claim for\nall nameplates, even ones which they've allocated themselves.\n\nNameplates (on the server) must live until the second client has learned\nabout the associated mailbox, after which point they can be reused by other\nclients. So if two clients connect quickly, but then maintain a long-lived\nwormhole connection, the do not need to consume the limited space of short\nnameplates for that whole time.\n\nThe `allocate` command allocates a nameplate (the server returns one that is\nas short as possible), and the `allocated` response provides the answer.\nClients can also send a `list` command to get back a `nameplates` response\nwith all allocated nameplates for the bound AppID: this helps the code-input\ntab-completion feature know which prefixes to offer. The `nameplates`\nresponse returns a list of dictionaries, one per claimed nameplate, with at\nleast an `id` key in each one (with the nameplate string). Future versions\nmay record additional attributes in the nameplate records, specifically a\nwordlist identifier and a code length (again to help with code-completion on\nthe receiver).\n\n## Mailboxes\n\nThe server provides a single \"Mailbox\" to each pair of connecting Wormhole\nclients. This holds an unordered set of messages, delivered immediately to\nconnected clients, and queued for delivery to clients which connect later.\nMessages from both clients are merged together: clients use the included\n`side` identifier to distinguish echoes of their own messages from those\ncoming from the other client.\n\nEach mailbox is \"opened\" by some number of clients at a time, until all\nclients have closed it. Mailboxes are kept alive by either an open client, or\na Nameplate which points to the mailbox (so when a Nameplate is deleted from\ninactivity, the corresponding Mailbox will be too).\n\nThe `open` command both marks the mailbox as being opened by the bound side,\nand also adds the WebSocket as subscribed to that mailbox, so new messages\nare delivered immediately to the connected client. There is no explicit ack\nto the `open` command, but since all clients add a message to the mailbox as\nsoon as they connect, there will always be a `message` reponse shortly after\nthe `open` goes through. The `close` command provokes a `closed` response.\n\nThe `close` command accepts an optional \"mood\" string: this allows clients to\ntell the server (in general terms) about their experiences with the wormhole\ninteraction. The server records the mood in its \"usage\" record, so the server\noperator can get a sense of how many connections are succeeding and failing.\nThe moods currently recognized by the Rendezvous Server are:\n\n* `happy` (default): the PAKE key-establishment worked, and the client saw at\n  least one valid encrypted message from its peer\n* `lonely`: the client gave up without hearing anything from its peer\n* `scary`: the client saw an invalid encrypted message from its peer,\n  indicating that either the wormhole code was typed in wrong, or an attacker\n  tried (and failed) to guess the code\n* `errory`: the client encountered some other error: protocol problem or\n  internal error\n\nThe server will also record `pruney` if it deleted the mailbox due to\ninactivity, or `crowded` if more than two sides tried to access the mailbox.\n\nWhen clients use the `add` command to add a client-to-client message, they\nwill put the body (a bytestring) into the command as a hex-encoded string in\nthe `body` key. They will also put the message's \"phase\", as a string, into\nthe `phase` key. See client-protocol.md for details about how different\nphases are used.\n\nWhen a client sends `open`, it will get back a `message` response for every\nmessage in the mailbox. It will also get a real-time `message` for every\n`add` performed by clients later. These `message` responses include \"side\"\nand \"phase\" from the sending client, and \"body\" (as a hex string, encoding\nthe binary message body). The decoded \"body\" will either by a random-looking\ncryptographic value (for the PAKE message), or a random-looking encrypted\nblob (for the VERSION message, as well as all application-provided payloads).\nThe `message` response will also include `id`, copied from the `id` of the\n`add` message (and used only by the timing-diagram tool).\n\nThe Rendezvous Server does not de-duplicate messages, nor does it retain\nordering: clients must do both if they need to.\n\n## All Message Types\n\nThis lists all message types, along with the type-specific keys for each (if\nany), and which ones provoke direct responses:\n\n* S->C welcome {welcome:}\n* (C->S) bind {appid:, side:}\n* (C->S) list {} -> nameplates\n* S->C nameplates {nameplates: [{id: str},..]}\n* (C->S) allocate {} -> allocated\n* S->C allocated {nameplate:}\n* (C->S) claim {nameplate:} -> claimed\n* S->C claimed {mailbox:}\n* (C->S) release {nameplate:?} -> released\n* S->C released\n* (C->S) open {mailbox:}\n* (C->S) add {phase: str, body: hex} -> message (to all connected clients)\n* S->C message {side:, phase:, body:, id:}\n* (C->S) close {mailbox:?, mood:?} -> closed\n* S->C closed\n* S->C ack\n* (C->S) ping {ping: int} -> ping\n* S->C pong {pong: int}\n* S->C error {error: str, orig:}\n\n## Persistence\n\nThe server stores all messages in a database, so it should not lose any\ninformation when it is restarted. The server will not send a direct\nresponse until any side-effects (such as the message being added to the\nmailbox) have been safely committed to the database.\n\nThe client library knows how to resume the protocol after a reconnection\nevent, assuming the client process itself continues to run.\n\nClients which terminate entirely between messages (e.g. a secure chat\napplication, which requires multiple wormhole messages to exchange\naddress-book entries, and which must function even if the two apps are never\nboth running at the same time) can use \"Journal Mode\" to ensure forward\nprogress is made: see \"journal.md\" for details.\n"
  },
  {
    "path": "docs/states.dot",
    "content": "/*digraph {\n    title [label=\"Mailbox\\lServer Machine\" style=\"dotted\"]\n\n    start -> opened [label=\"open(side)\"];\n\n    opened -> opened [label=\"open(side)\"];\n    opened -> opened [label=\"add_message(sided_message)\"];\n    opened -> closing [label=\"close(side, mood)\"];\n\n    closing -> closing [label=\"close(side, mood)\"];\n}\n*/\n\n\n// note: all messages have an \"id\" and a \"type\"\n// and the server sends back an \"ack\" for every one\n// but that ack etc isn't covered in these diagrams\n\ndigraph {\n    node [fontname = \"Source Sans Pro\" fontsize = 22];\n    edge [fontname = \"Source Code Pro\" fontsize = 18 fontcolor=blue];\n    graph [fontname = \"Source Sans Pro\" fontsize = 22];\n\n    title [label=\"Mailbox Server\" style=\"dotted\" fontsize=32];\n\n    ranksep = 1;\n\n    start [label=\"START\\n(perm=none)\"];\n    start_permissions [label=\"START\\n(perm=hashcash)\"];\n    start_reconn [label=\"RECONNECT\\n(mailbox, perm=none)\"];\n    start_reconn_perm [label=\"RECONNECT\\n(mailbox, perm=hashcash)\"];\n    done [label=\"DONE\\nmood=\" shape=box style=filled];\n\n    {rank=same; start start_permissions start_reconn start_reconn_perm}\n    start [shape=box, style=bold];\n    start -> bound [label=\"bind(appid, side)\"];\n\n    # blue, to match seqdiag section on \"nameplate allocated\"\n    bound [fillcolor=cadetblue1, style=filled];\n    have_nameplate [fillcolor=cadetblue1, style=filled];\n    claimed [fillcolor=cadetblue1, style=filled];\n\n    start_permissions [shape=box, style=bold];\n    start_permissions -> granted [label=\"submit_permissions()\" fontcolor=red];\n    granted -> bound [label=\"bind(appid, side)\"];\n\n    start_reconn [shape=box, style=bold];\n    start_reconn -> open [label=\"open(mailbox_id)\\l-> messages: send(side, phase, body)\\l\"];\n\n    start_reconn_perm [shape=box, style=bold];\n    start_reconn_perm -> reconn_granted [label=\"submit_permissions()\" fontcolor=red];\n    open [fillcolor=green style=filled];\n    reconn_granted -> open [label=\"open(mailbox_id)\\l-> messages: send(side, phase, body)\\l\"];\n\n    bound -> have_nameplate [label=\"allocate()\\l-> nameplate_id\\l\"]\n    # allocate() really does do a claim() .. but you have to call it explicitly too\n    have_nameplate -> claimed [label=\"claim(nameplate, side)\\l-> mailbox_id\\l\" fontcolor=darkgreen]\n    have_nameplate -> done [label=\"release(nameplate)\" fontcolor=red]\n\n    # ths is on the \"join\" side; they are told the nameplate number\n    bound -> claimed [label=\"claim(nameplate, side)\\l-> mailbox_id\\l\" fontcolor=darkgreen]\n    claimed -> unclaimed [label=\"release(nameplate)\" fontcolor=red]\n\n    # note: allowing two different paths to 'unclaimed' is I think\n    # _allowed_ currently by the server, but better to define it with\n    # juts one way probably.\n\n    unclaimed -> open [label=\"open(mailbox_id)\\l-> messages: send(side, phase, body)\\l\"]\n    #claimed -> open [label=\"open(mailbox_id)\\l-> send(all_messages)\\l\"]\n    #open -> open [label=\"release(nameplate)\"]\n    open -> open [label=\"add_message(msg)\\l-> send(side, phase, body)\\l\"]\n    open ->      done [label=\"close(mailbox_id)\" fontcolor=red]\n    # XXX will get all message already in the box, how to represent?\n}"
  },
  {
    "path": "docs/welcome.md",
    "content": "# Magic Wormhole Mailbox Server\n[![Build Status](https://travis-ci.org/warner/magic-wormhole-mailbox-server.svg?branch=master)](https://travis-ci.org/warner/magic-wormhole-mailbox-server)\n[![Windows Build Status](https://ci.appveyor.com/api/projects/status/mfnn5rsyfnrq576a/branch/master?svg=true)](https://ci.appveyor.com/project/warner/magic-wormhole-mailbox-server)\n[![codecov.io](https://codecov.io/github/warner/magic-wormhole-mailbox-server/coverage.svg?branch=master)](https://codecov.io/github/warner/magic-wormhole-mailbox-server?branch=master)\n\nThis repository holds the code for the main server that\n[Magic-Wormhole](http://magic-wormhole.io) clients connect to. The server\nperforms store-and-forward delivery for small key-exchange and control\nmessages. Bulk data is sent over a direct TCP connection, or through a\n[transit-relay](https://github.com/warner/magic-wormhole-transit-relay).\n\nClients connect with WebSockets, for low-latency delivery in the happy case\nwhere both clients are attached at the same time. Message are stored in to\nenable non-simultaneous clients to make forward progress. The server uses a\nsmall SQLite database for persistence (and clients will reconnect\nautomatically, allowing the server to be rebooted without losing state). An\noptional \"usage DB\" tracks historical activity for status monitoring and\noperational maintenance.\n\n## Running A Server\n\nNote that the standard [Magic-Wormhole](http://magic-wormhole.io)\ncommand-line tool is preconfigured to use a mailbox server hosted by the\nproject, so running your own server is only necessary for custom applications\nthat use magic-wormhole as a library.\n\nThe mailbox server is deployed as a twist/twistd plugin. Running a basic\nserver looks like this:\n\n```\ntwist wormhole-mailbox --usage-db=usage.sqlite\n```\n\nUse ``twist wormhole-mailbox --help`` for more details.\n\nIf you use the default ``--port=tcp:4000``, on a machine named\n``example.com``, then clients can reach your server with the following\noption:\n\n```\nwormhole --relay-url=ws://example.com:4000/v1 send FILENAME\n```\n\n## License, Compatibility\n\nThis library is released under the MIT license, see LICENSE for details.\n\nThis library is compatible with python2.7, 3.4, 3.5, and 3.6 .\n\n"
  },
  {
    "path": "misc/migrate_channel_db.py",
    "content": "\"\"\"Migrate the channel data from the old bundled Mailbox Server database.\n\nThe magic-wormhole package used to include both servers (Rendezvous and\nTransit). \"wormhole server\" started both of these, and used the\n\"relay.sqlite\" database to store both immediate server state and long-term\nusage data.\n\nThese were split out to their own packages: version 0.11 omitted the Transit\nRelay, and 0.12 removed the Mailbox Server in favor of the new\n\"magic-wormhole-mailbox-server\" distribution.\n\nThis script reads the short-term channel data from the pre-0.12\nwormhole-server relay.sqlite, and copies it into a new \"relay.sqlite\"\ndatabase in the current directory.\n\nIt will refuse to touch an existing \"relay.sqlite\" file.\n\nThe resuting \"relay.sqlite\" should be passed into --channel-db=, e.g. \"twist\nwormhole-mailbox --channel-db=.../PATH/TO/relay.sqlite\". However in most\ncases you can just store it in the default location of \"./relay.sqlite\" and\nomit the --channel-db= argument.\n\nNote that an idle server will have no channel data, so you could instead just\nwait for the server to be empty (sqlite3 relay.sqlite message |grep INSERT).\n\"\"\"\n\nimport sys\nfrom wormhole_mailbox_server.database import (open_existing_db,\n                                              create_channel_db)\n\nsource_fn = sys.argv[1]\nsource_db = open_existing_db(source_fn)\ntarget_db = create_channel_db(\"relay.sqlite\")\n\nnum_rows = 0\n\nfor row in source_db.execute(\"SELECT * FROM `mailboxes`\").fetchall():\n    target_db.execute(\"INSERT INTO `mailboxes`\"\n                      \" (`app_id`, `id`, `updated`, `for_nameplate`)\"\n                      \" VALUES(?,?,?,?)\",\n                      (row[\"app_id\"], row[\"id\"], row[\"updated\"],\n                       row[\"for_nameplate\"]))\n    num_rows += 1\n\nfor row in source_db.execute(\"SELECT * FROM `mailbox_sides`\").fetchall():\n    target_db.execute(\"INSERT INTO `mailbox_sides`\"\n                      \" (`mailbox_id`, `opened`, `side`, `added`, `mood`)\"\n                      \" VALUES(?,?,?,?,?)\",\n                      (row[\"mailbox_id\"], row[\"opened\"], row[\"side\"],\n                       row[\"added\"], row[\"mood\"]))\n    num_rows += 1\n\nfor row in source_db.execute(\"SELECT * FROM `nameplates`\").fetchall():\n    target_db.execute(\"INSERT INTO `nameplates`\"\n                      \" (`id`, `app_id`, `name`, `mailbox_id`, `request_id`)\"\n                      \" VALUES(?,?,?,?,?)\",\n                      (row[\"id\"], row[\"app_id\"], row[\"name\"],\n                       row[\"mailbox_id\"], row[\"request_id\"]))\n    num_rows += 1\n\nfor row in source_db.execute(\"SELECT * FROM `nameplate_sides`\").fetchall():\n    target_db.execute(\"INSERT INTO `nameplate_sides`\"\n                      \" (`nameplates_id`, `claimed`, `side`, `added`)\"\n                      \" VALUES(?,?,?,?)\",\n                      (row[\"nameplates_id\"], row[\"claimed\"], row[\"side\"],\n                       row[\"added\"]))\n    num_rows += 1\n\nfor row in source_db.execute(\"SELECT * FROM `messages`\").fetchall():\n    target_db.execute(\"INSERT INTO `messages`\"\n                      \" (`app_id`, `mailbox_id`, `side`, `phase`, `body`, \"\n                      \"  `server_rx`, `msg_id`)\"\n                      \" VALUES(?,?,?,?,?,?,?)\",\n                      (row[\"app_id\"], row[\"mailbox_id\"], row[\"side\"],\n                       row[\"phase\"], row[\"body\"],\n                       row[\"server_rx\"], row[\"msg_id\"]))\n    num_rows += 1\ntarget_db.commit()\n\nprint(\"channel database migrated (%d rows) into 'relay.sqlite'\" % num_rows)\nsys.exit(0)\n"
  },
  {
    "path": "misc/migrate_usage_db.py",
    "content": "\"\"\"Migrate the usage data from the old bundled Mailbox Server database.\n\nThe magic-wormhole package used to include both servers (Rendezvous and\nTransit). \"wormhole server\" started both of these, and used the\n\"relay.sqlite\" database to store both immediate server state and long-term\nusage data.\n\nThese were split out to their own packages: version 0.11 omitted the Transit\nRelay, and 0.12 removed the Mailbox Server in favor of the new\n\"magic-wormhole-mailbox-server\" distribution.\n\nThis script reads the long-term usage data from the pre-0.12 wormhole-server\nrelay.sqlite, and copies it into a new \"usage.sqlite\" database in the current\ndirectory.\n\nIt will refuse to touch an existing \"usage.sqlite\" file.\n\nThe resuting \"usage.sqlite\" should be passed into --usage-db=, e.g. \"twist\nwormhole-mailbox --usage-db=.../PATH/TO/usage.sqlite\".\n\"\"\"\n\nimport sys\nfrom wormhole_mailbox_server.database import open_existing_db, create_usage_db\n\nsource_fn = sys.argv[1]\nsource_db = open_existing_db(source_fn)\ntarget_db = create_usage_db(\"usage.sqlite\")\n\nnum_nameplate_rows = 0\nfor row in source_db.execute(\"SELECT * FROM `nameplate_usage`\"\n                             \" ORDER BY `started`\").fetchall():\n    target_db.execute(\"INSERT INTO `nameplates`\"\n                      \" (`app_id`, `started`, `waiting_time`,\"\n                      \"  `total_time`, `result`)\"\n                      \" VALUES(?,?,?,?,?)\",\n                      (row[\"app_id\"], row[\"started\"], row[\"waiting_time\"],\n                       row[\"total_time\"], row[\"result\"]))\n    num_nameplate_rows += 1\n\n\nnum_mailbox_rows = 0\nfor row in source_db.execute(\"SELECT * FROM `mailbox_usage`\"\n                             \" ORDER BY `started`\").fetchall():\n    target_db.execute(\"INSERT INTO `mailboxes`\"\n                      \" (`app_id`, `for_nameplate`,\"\n                      \" `started`, `total_time`, `waiting_time`,\"\n                      \"  `result`)\"\n                      \" VALUES(?,?,?,?,?,?)\",\n                      (row[\"app_id\"], row[\"for_nameplate\"],\n                       row[\"started\"], row[\"total_time\"], row[\"waiting_time\"],\n                       row[\"result\"]))\n    num_mailbox_rows += 1\n\ntarget_db.execute(\"INSERT INTO `current`\"\n                  \" (`rebooted`, `updated`, `blur_time`,\"\n                  \"  `connections_websocket`)\"\n                  \" VALUES(?,?,?,?)\",\n                  (0, 0, 0, 0))\ntarget_db.commit()\n\nprint(\"usage database migrated (%d+%d rows) into 'usage.sqlite'\" % (num_nameplate_rows, num_mailbox_rows))\nsys.exit(0)\n"
  },
  {
    "path": "misc/munin/wormhole_active",
    "content": "#! /usr/bin/env python3\n\n\"\"\"\nUse the following in /etc/munin/plugin-conf.d/wormhole :\n\n[wormhole_*]\nenv.channeldb /path/to/your/wormhole/server/channel.sqlite\nenv.usagedb /path/to/your/wormhole/server/usage.sqlite\n\"\"\"\n\nfrom __future__ import print_function\nimport os, sys, time, sqlite3\n\nCONFIG = \"\"\"\\\ngraph_title Magic-Wormhole Active Channels\ngraph_vlabel Channels\ngraph_category wormhole\nnameplates.label Nameplates\nnameplates.draw LINE2\nnameplates.type GAUGE\nmailboxes.label Mailboxes\nmailboxes.draw LINE2\nmailboxes.type GAUGE\nmessages.label Messages\nmessages.draw LINE1\nmessages.type GAUGE\n\"\"\"\n\nif len(sys.argv) > 1 and sys.argv[1] == \"config\":\n    print(CONFIG.rstrip())\n    sys.exit(0)\n\nusagedbfile = os.environ[\"usagedb\"]\nassert os.path.exists(usagedbfile)\nusage_db = sqlite3.connect(\"file:%s?mode=ro\" % usagedbfile, uri=True)\n\nchanneldbfile = os.environ[\"channeldb\"]\nassert os.path.exists(channeldbfile)\nchannel_db = sqlite3.connect(\"file:%s?mode=ro\" % channeldbfile, uri=True)\n\nMINUTE = 60.0\nupdated,rebooted = usage_db.execute(\"SELECT `updated`,`rebooted` FROM `current`\").fetchone()\nif time.time() > updated + 6*MINUTE:\n    sys.exit(1) # expired\n\nnameplates = channel_db.execute(\"SELECT COUNT() FROM `nameplates`\").fetchone()[0]\nmailboxes = channel_db.execute(\"SELECT COUNT() FROM `mailboxes`\").fetchone()[0]\nmessages = channel_db.execute(\"SELECT COUNT() FROM `messages`\").fetchone()[0]\n\nprint(\"nameplates.value\", nameplates)\nprint(\"mailboxes.value\", mailboxes)\nprint(\"messages.value\", messages)\n"
  },
  {
    "path": "misc/munin/wormhole_errors",
    "content": "#! /usr/bin/env python3\n\n\"\"\"\nUse the following in /etc/munin/plugin-conf.d/wormhole :\n\n[wormhole_*]\nenv.usagedb /path/to/your/wormhole/server/usage.sqlite\n\"\"\"\n\nfrom __future__ import print_function\nimport os, sys, time, sqlite3\n\nCONFIG = \"\"\"\\\ngraph_title Magic-Wormhole Server Errors\ngraph_vlabel Events Since Reboot\ngraph_category wormhole\nnameplates.label Nameplate Errors (total)\nnameplates.draw LINE1\nnameplates.type GAUGE\nmailboxes.label Mailboxes (total)\nmailboxes.draw LINE1\nmailboxes.type GAUGE\nmailboxes_scary.label Mailboxes (scary)\nmailboxes_scary.draw LINE1\nmailboxes_scary.type GAUGE\n\"\"\"\n\nif len(sys.argv) > 1 and sys.argv[1] == \"config\":\n    print(CONFIG.rstrip())\n    sys.exit(0)\n\nusagedbfile = os.environ[\"usagedb\"]\nassert os.path.exists(usagedbfile)\nusage_db = sqlite3.connect(\"file:%s?mode=ro\" % usagedbfile, uri=True)\n\nMINUTE = 60.0\nupdated,rebooted = usage_db.execute(\"SELECT `updated`,`rebooted` FROM `current`\").fetchone()\nif time.time() > updated + 6*MINUTE:\n    sys.exit(1) # expired\n\nr1 = usage_db.execute(\"SELECT COUNT() FROM `nameplates` WHERE `started` >= ?\",\n                      (rebooted,)).fetchone()[0]\nr2 = usage_db.execute(\"SELECT COUNT() FROM `nameplates`\"\n                      \" WHERE `started` >= ?\"\n                      \"  AND `result` = 'happy'\",\n                      (rebooted,)).fetchone()[0]\nprint(\"nameplates.value\", (r1 - r2))\nr1 = usage_db.execute(\"SELECT COUNT() FROM `mailboxes` WHERE `started` >= ?\",\n                      (rebooted,)).fetchone()[0]\nr2 = usage_db.execute(\"SELECT COUNT() FROM `mailboxes` WHERE `started` >= ?\"\n                      \" AND `result` = 'happy'\",\n                      (rebooted,)).fetchone()[0]\nprint(\"mailboxes.value\", (r1 - r2))\nr = usage_db.execute(\"SELECT COUNT() FROM `mailboxes` WHERE `started` >= ?\"\n                     \" AND `result` = 'scary'\",\n                     (rebooted,)).fetchone()[0]\nprint(\"mailboxes_scary.value\", r)\n"
  },
  {
    "path": "misc/munin/wormhole_event_rate",
    "content": "#! /usr/bin/env python3\n\n\"\"\"\nUse the following in /etc/munin/plugin-conf.d/wormhole :\n\n[wormhole_*]\nenv.usagedb /path/to/your/wormhole/server/usage.sqlite\n\"\"\"\n\nfrom __future__ import print_function\nimport os, sys, time, sqlite3\nfrom collections import defaultdict\n\nCONFIG = \"\"\"\\\ngraph_title Magic-Wormhole Server Events\ngraph_vlabel Events per Hour\ngraph_category wormhole\nhappy.label Happy\nhappy.draw LINE\nhappy.type DERIVE\nhappy.min 0\nhappy.max 60\nhappy.cdef happy,3600,*\nincomplete.label Incomplete\nincomplete.draw LINE\nincomplete.type DERIVE\nincomplete.min 0\nincomplete.max 60\nincomplete.cdef incomplete,3600,*\nscary.label Scary\nscary.draw LINE\nscary.type DERIVE\nscary.min 0\nscary.max 60\nscary.cdef scary,3600,*\n\"\"\"\n\nif len(sys.argv) > 1 and sys.argv[1] == \"config\":\n    print(CONFIG.rstrip())\n    sys.exit(0)\n\nusagedbfile = os.environ[\"usagedb\"]\nassert os.path.exists(usagedbfile)\nusage_db = sqlite3.connect(\"file:%s?mode=ro\" % usagedbfile, uri=True)\n\nMINUTE = 60.0\nupdated,rebooted = usage_db.execute(\"SELECT `updated`,`rebooted` FROM `current`\").fetchone()\nif time.time() > updated + 6*MINUTE:\n    sys.exit(1) # expired\n\natm = defaultdict(int)\nfor mood in [\"happy\", \"scary\", \"lonely\", \"errory\", \"pruney\", \"crowded\"]:\n    atm[mood] = usage_db.execute(\"SELECT COUNT() FROM `mailboxes`\"\n                                 \" WHERE `result` = ?\", (mood,)).fetchone()[0]\n\nprint(\"happy.value\", atm[\"happy\"])\nprint(\"incomplete.value\", (atm[\"pruney\"] + atm[\"lonely\"]))\nprint(\"scary.value\", atm[\"scary\"])\n"
  },
  {
    "path": "misc/munin/wormhole_events",
    "content": "#! /usr/bin/env python3\n\n\"\"\"\nUse the following in /etc/munin/plugin-conf.d/wormhole :\n\n[wormhole_*]\nenv.usagedb /path/to/your/wormhole/server/usage.sqlite\n\"\"\"\n\nfrom __future__ import print_function\nimport os, sys, time, sqlite3\n\nCONFIG = \"\"\"\\\ngraph_title Magic-Wormhole Mailbox Events (since reboot)\ngraph_vlabel Events Since Reboot\ngraph_category wormhole\nhappy.label Happy\nhappy.draw LINE2\nhappy.type GAUGE\ntotal.label Total\ntotal.draw LINE1\ntotal.type GAUGE\nscary.label Scary\nscary.draw LINE2\nscary.type GAUGE\npruney.label Pruney\npruney.draw LINE1\npruney.type GAUGE\nlonely.label Lonely\nlonely.draw LINE2\nlonely.type GAUGE\nerrory.label Errory\nerrory.draw LINE1\nerrory.type GAUGE\n\"\"\"\n\nif len(sys.argv) > 1 and sys.argv[1] == \"config\":\n    print(CONFIG.rstrip())\n    sys.exit(0)\n\nusagedbfile = os.environ[\"usagedb\"]\nassert os.path.exists(usagedbfile)\nusage_db = sqlite3.connect(\"file:%s?mode=ro\" % usagedbfile, uri=True)\n\nMINUTE = 60.0\nupdated,rebooted,blur = usage_db.execute(\n    \"SELECT `updated`,`rebooted`,`blur_time` FROM `current`\").fetchone()\nif time.time() > updated + 6*MINUTE:\n    sys.exit(1) # expired\nif blur is not None:\n    rebooted = blur * (rebooted // blur)\n    # After a reboot, the operator will get to see events that happen during\n    # the first blur window (without this adjustment, those events would be\n    # hidden since they'd appear to start before the reboot). The downside is\n    # that the counter won't drop down to zero at a reboot (if there are recent\n    # events).\n\n#r = usage_db.execute(\"SELECT COUNT(`mood`) FROM `mailboxes` WHERE `started` > ?\",\n#                     (rebooted,)).fetchone()\nfor mood in [\"happy\", \"scary\", \"lonely\", \"errory\", \"pruney\", \"crowded\"]:\n    r = usage_db.execute(\"SELECT COUNT() FROM `mailboxes` WHERE `started` >= ?\"\n                         \" AND `result` = ?\",\n                        (rebooted, mood)).fetchone()[0]\n    print(\"%s.value\" % mood, r)\nr = usage_db.execute(\"SELECT COUNT() FROM `mailboxes` WHERE `started` >= ?\",\n                     (rebooted,)).fetchone()[0]\nprint(\"total.value\", r)\n"
  },
  {
    "path": "misc/munin/wormhole_events_alltime",
    "content": "#! /usr/bin/env python3\n\n\"\"\"\nUse the following in /etc/munin/plugin-conf.d/wormhole :\n\n[wormhole_*]\nenv.usagedb /path/to/your/wormhole/server/usage.sqlite\n\"\"\"\n\nfrom __future__ import print_function\nimport os, sys, time, sqlite3\n\nCONFIG = \"\"\"\\\ngraph_title Magic-Wormhole Mailbox Events (all time)\ngraph_vlabel Events Since DB Creation\ngraph_category wormhole\nhappy.label Happy\nhappy.draw LINE2\nhappy.type GAUGE\ntotal.label Total\ntotal.draw LINE1\ntotal.type GAUGE\nscary.label Scary\nscary.draw LINE2\nscary.type GAUGE\npruney.label Pruney\npruney.draw LINE1\npruney.type GAUGE\nlonely.label Lonely\nlonely.draw LINE2\nlonely.type GAUGE\nerrory.label Errory\nerrory.draw LINE1\nerrory.type GAUGE\n\"\"\"\n\nif len(sys.argv) > 1 and sys.argv[1] == \"config\":\n    print(CONFIG.rstrip())\n    sys.exit(0)\n\nusagedbfile = os.environ[\"usagedb\"]\nassert os.path.exists(usagedbfile)\nusage_db = sqlite3.connect(\"file:%s?mode=ro\" % usagedbfile, uri=True)\n\nMINUTE = 60.0\nupdated,rebooted = usage_db.execute(\"SELECT `updated`,`rebooted` FROM `current`\").fetchone()\nif time.time() > updated + 6*MINUTE:\n    sys.exit(1) # expired\n\nfor mood in [\"happy\", \"scary\", \"lonely\", \"errory\", \"pruney\", \"crowded\"]:\n    r = usage_db.execute(\"SELECT COUNT() FROM `mailboxes` WHERE `result` = ?\",\n                        (mood,)).fetchone()[0]\n    print(\"%s.value\" % mood, r)\nr = usage_db.execute(\"SELECT COUNT() FROM `mailboxes`\").fetchone()[0]\nprint(\"total.value\", r)\n"
  },
  {
    "path": "misc/munin/wormhole_version_uptake",
    "content": "#! /usr/bin/env python3\n\n\"\"\"\nUse the following in /etc/munin/plugin-conf.d/wormhole :\n\n[wormhole_*]\nenv.usagedb /path/to/your/wormhole/server/usage.sqlite\nenv.python_client_versions = 0.11.0\n\nThe python_client_versions list will be used to choose what to graph: any\npython client which reports an application version not on the list will be\nlisted as 'other', and all non-python clients will be listed as 'non-python',\nand clients which don't report a version at all will be listed as 'unknown'.\nThis list should grow over time just before new versions are released, so the\ngraph will remain sorted and stable.\n\"\"\"\n\nfrom __future__ import print_function\nimport os, sys, time, sqlite3, collections\n\nCONFIG = \"\"\"\\\ngraph_title Magic-Wormhole Version Uptake\ngraph_vlabel Clients\ngraph_category wormhole\n\"\"\"\nversions = [\"unknown\", \"non-python\", \"other\"]\nif \"python_client_versions\" in os.environ:\n    versions.extend(os.environ[\"python_client_versions\"].split(\",\"))\nnames = dict([(v, (\"v_\" + v).replace(\".\", \"_\").replace(\"-\", \"_\"))\n              for v in versions])\n\nif len(sys.argv) > 1 and sys.argv[1] == \"config\":\n    print(CONFIG.rstrip())\n    first = True\n    for v in versions:\n        name = names[v]\n        print(\"%s.label %s\" % (name, v))\n        if first:\n            print(\"%s.draw AREA\" % name)\n            first = False\n        else:\n            print(\"%s.draw STACK\" % name)\n        print(\"%s.type GAUGE\" % name)\n    sys.exit(0)\n\nusagedbfile = os.environ[\"usagedb\"]\nassert os.path.exists(usagedbfile)\nusage_db = sqlite3.connect(\"file:%s?mode=ro\" % usagedbfile, uri=True)\n\nnow = time.time()\nMINUTE = 60.0\nupdated,rebooted = usage_db.execute(\"SELECT `updated`,`rebooted` FROM `current`\").fetchone()\nif now > updated + 6*MINUTE:\n    sys.exit(1) # expired\n\ndef dict_factory(cursor, row):\n    d = {}\n    for idx, col in enumerate(cursor.description):\n        d[col[0]] = row[idx]\n    return d\nusage_db.row_factory = dict_factory\n\nseen_sides = set()\ncounts = collections.defaultdict(int)\nfor row in usage_db.execute(\"SELECT * FROM `client_versions`\"\n                            \" WHERE (`connect_time` > ? AND `connect_time` < ?)\",\n                            (now - 60*MINUTE, now)).fetchall():\n    if row[\"side\"] in seen_sides:\n        continue\n    seen_sides.add(row[\"side\"])\n    if row[\"implementation\"] is None and row[\"version\"] is None:\n        version = \"unknown\"\n    elif row[\"implementation\"] != \"python\":\n        version = \"non-python\"\n    elif row[\"version\"] in versions:\n        version = row[\"version\"]\n    else:\n        version = \"other\"\n    counts[version] += 1\n\nfor version in versions:\n    print(\"%s.value\" % names[version], counts[version])\n"
  },
  {
    "path": "misc/windows-build.cmd",
    "content": "@echo off\n:: To build extensions for 64 bit Python 3, we need to configure environment\n:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:\n:: MS Windows SDK for Windows 7 and .NET Framework 4\n::\n:: More details at:\n:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows\n\nIF \"%DISTUTILS_USE_SDK%\"==\"1\" (\n    ECHO Configuring environment to build with MSVC on a 64bit architecture\n    ECHO Using Windows SDK 7.1\n    \"C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1\\Setup\\WindowsSdkVer.exe\" -q -version:v7.1\n    CALL \"C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1\\Bin\\SetEnv.cmd\" /x64 /release\n    SET MSSdk=1\n    REM Need the following to allow tox to see the SDK compiler\n    SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB\n) ELSE (\n    ECHO Using default MSVC build environment\n)\n\nCALL %*\n"
  },
  {
    "path": "newest-version.py",
    "content": "#\n# print out the most-recent version\n#\n\nfrom dulwich.repo import Repo\nfrom dulwich.porcelain import tag_list\n\n\ndef existing_tags(git):\n    versions = [\n        tuple(map(int, v.decode(\"utf8\").split(\".\")))\n        for v in tag_list(git)\n    ]\n    return versions\n\n\ndef main():\n    git = Repo(\".\")\n    print(\"{}.{}.{}\".format(*sorted(existing_tags(git))[-1]))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "setup.cfg",
    "content": "[versioneer]\nVCS = git\nversionfile_source = src/wormhole_mailbox_server/_version.py\nversionfile_build = wormhole_mailbox_server/_version.py\ntag_prefix = \nparentdir_prefix = magic-wormhole-mailbox-server\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\n\nimport versioneer\n\ncommands = versioneer.get_cmdclass()\n\ntrove_classifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Environment :: Console\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Topic :: Security :: Cryptography\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n    ]\n\nsetup(name=\"magic-wormhole-mailbox-server\",\n      version=versioneer.get_version(),\n      description=\"Securely transfer data between computers\",\n      long_description=open('README.md').read(),\n      long_description_content_type='text/markdown',\n      author=\"Brian Warner\",\n      author_email=\"warner-magic-wormhole@lothar.com\",\n      license=\"MIT\",\n      url=\"https://github.com/warner/magic-wormhole-mailbox-server\",\n      classifiers=trove_classifiers,\n      package_dir={\"\": \"src\"},\n      packages=[\"wormhole_mailbox_server\",\n                \"wormhole_mailbox_server.test\",\n                \"twisted.plugins\",\n                ],\n      package_data={\"wormhole_mailbox_server\": [\"db-schemas/*.sql\"]},\n      install_requires=[\n          \"attrs >= 16.3.0\", # 16.3.0 adds __attrs_post_init__\n          \"twisted[tls] >= 17.5.0\",\n          \"autobahn[twisted] >= 0.14.1\",\n          \"setuptools\", # pkg_resources\n      ],\n      extras_require={\n          ':sys_platform==\"win32\"': [\"pywin32\"],\n          \"dev\": [\"treq\", \"tox\", \"pyflakes\"],\n          \"release\": [\"dulwich\", \"docutils\", \"wheel\"],\n      },\n      test_suite=\"wormhole_mailbox_server.test\",\n      cmdclass=commands,\n      )\n"
  },
  {
    "path": "signatures/magic-wormhole-mailbox-server-0.5.0.tar.gz.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmcsEawRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaafF2ggAsgMKP6ZyJ1sqJA58trJaufuV7ypqDJyV\nUvPcIMHjF55YIJ2CRXt3fO6QFxiG/WHTWswENKxvFEp2F5ZCe13XLZwugX62/6Hc\nT1jCIwvjU93yiEdqPvtMcAX5FWJUMKdOmlCqm/sfP5gF7D34O3vsM6wxbF8YlNFo\nNo0zZvMDxAlPmNER7iTujnckw5jyHqHSFn5AhWqigJTQlB3Mac7eqXuMIuCCOdy+\n8PBpv0+jpdvzuq9hTFNvErKvg/Sy37nC1PJkteIXbneQiJjbVcvK4qniROzDbrnp\nzbI+WEtCsm7o3ieLxt5P11fPjO+4/Tf9LwyjmkGnn265fUwrHFMx7g==\n=5dEl\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic-wormhole-mailbox-server-0.5.1.tar.gz.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmcwGhcRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaaeh4wgAulK5GcWLnePJwUrmKp+2J57zAUOLOONY\n218k8Tw6a7DfnbNCmx0+HnSu1zrpty8aGVuNynR3fMeW9pl140ZFYQ+aU96dqaR5\nVHt4zbX4o4ZtBb/qxOsKpGljpv3a+47RSrdcpF6zPCVnqz219OykIKgSyURU2DMi\n3tqGWGyoES97Rau6P1B9TrNCRdoC9+ajrk74gggkcnXnIPqyLl0KH2CvP+DNzsBA\nYEECui2Rqu2WXSYIUY0HuWYP+fCuurqGLhlY3JYVDzUwrO2bK7brOW0lG4B/gZYy\nmxBFUh/OVOx3UdBpMJrYFOrH1JVZIFBmu3sF1EUUffM1VndPErIn7w==\n=kkQZ\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic_wormhole_mailbox_server-0.5.0-py3-none-any.whl.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmcsEasRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaacL7QgAnFo9ilYlzWRlaXBPu/nx86lWzMP8zOKM\nLVD94B6wj3vXqosKpe7I3qmGWZrfo0vjHojzzh7GlUMIyapAB4dQ9jJknKOv7cA1\ninrCLzObEVJ2JbnZexR7GIwMNhqIWn/PLd1YNygjn9u/sdLTvheGwmZ4vDBIfuTe\nw17QRt5Tne2RjBgpNuJBmCz84AQ0TuwW+9ABU3DO6pcFdBQrNYvgmJkUcj8tYfdr\nq+FiN5UIEZT0eBPftawY6LA8J7IYJOZk2035KHfxbejA1z2FmogszP1cGhefj/5G\nAARkxkoo83Dx18sqlU1h/2vkH8LNwm8U789qm6divYAjrA+hc/J8zw==\n=CC6t\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic_wormhole_mailbox_server-0.5.1-py3-none-any.whl.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmcwGhcRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaaeUfQf/fiRlkCg7buxgODywLgdFOIyPaHNLrCGY\nERmK5UsSSL5CT38e3I3EuNkmUmdQwEUC0DXU0y81QOoPVUC8U1mSqy50C/NnAWdG\nIj1MGYN5Zlp/8Ydt9oeW4CXYA4WkCfAxzD/rYkRwjHDgBW13gAgklIyK0E8Ssg5m\ni/32c21JGMnBt36o9yO3VBgqw9Vw6Cr9hoHOHt1xRYEZ4PPVNi/L0WKr9MjSPE+X\ntwxnKgbWfRVdvC2Lt0hUsFDqy24lGAjZ/n4hrmj+HOJ72D0oczJpFa+7gjr8j7tO\nnITTW0/5Tqm+405jSYybtmm8R033ZvMI+okNQ4luShs41kHzo4Odpw==\n=MiBZ\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic_wormhole_mailbox_server-0.6.0-py3-none-any.whl.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmmO09cRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaacP8wgArStS7wGiSCiBuEIfue5IStfPmIJOCpGA\nZVkVd3bNscioNv6Xwt7MQItbuKO544VR72uyJre4o/M8OqHjiNEct+0iEoZDIGhH\nPfLQ2qduTbZgcNe5NpwdAWcW7v1DHcnY8cSo1UHsXJZg5kZBv1EDVYlwaQi+OLjr\nnyD2AeAWf3HfohMdGxgqpcBCTXamCxDSHLjqPS0njgRK9HHpGnLEoxp8wiYPMah3\n0eCuJn/SxT2vsO7u5eQYrJ3bfqsUlu4wgelZE88LwLVmD3Pjj9DxMvFP+QPZ0ivv\nv3aYft23wET+4gZJP2/tb2zJBern18qohTD3vIakEjUtRDPyiQBXOg==\n=nicE\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic_wormhole_mailbox_server-0.6.0.tar.gz.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmmO09cRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaaeYjQf+P3CoVhbOz3V87wAglhn8RWIpRkFmr0oK\n4nuWxpVZ9SyIn1OAXto2jjoWQ+ABoN8goUuR233le+zNJ2i+td25gcBMekA0PwDT\ncwS53sgWhkrvhsEgYCSCv1Wnbgn3Y0Bo43fJWNEG3itEZgBVgoa6065s3/FF4BKE\nOOO3MjKaadBvPUM10MmM/TGvCEJOmQQ+yP0YjMY4+bHOS0Vv/QFVz5/asL0Nla7r\ndxBenpLHa46Y5J8vpYU0I7T6esWvZqc13Zvc41i3iTiPhFXnk+U9j9PrXfB6csbT\n29RJub8RcfwgegKCNBlKBbTb4FetEQp22sFptQs/5fQXH4AjIY8RyQ==\n=eRHe\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic_wormhole_mailbox_server-0.7.0-py3-none-any.whl.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmoGEmkRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaafI3QgAurb2bNlFd8MB1EOVJ8qYH0Ea06Mynuif\nUpowOziYAz28fcXYL/gErDFoJNP4yafe0N7NNQwd2QeFcL+suiIucR4EE71c0PKi\nJG99oJ3wpEwn/ADuShc+vM+kMGrPCUAHtEBAmUNRHgnAouVdjZYpz7joJcr/uy7R\nZCm6rYqoTqCexXPyax1iWUp5ZMK9fHSOFyoNKXbFDKLekVcyLI9XDInv92x2PCBx\n5gna/htnxtUpZGrtSTwe1Yx/hYLb57E7BBnTuWDFMhvH7nSTR/pvGc8hvkzB/hxB\n31hnRRrdY7snma2CqK07d2x8K1hORdih+ptHKPSI5kua3UJAnyJqTA==\n=Av5c\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic_wormhole_mailbox_server-0.7.0.tar.gz.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmoGEmoRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaacnRAf/dxdrzwCGqTyUU11lf0KwVYcLLgIAKFPJ\nPR3vvz66X5dUshKOGoE1IyL1xg0BCOFVnPzKl8ptaDXNPLKXreSRg0ALP6YHXNqt\nhhEg4P+GC2E9TkYe4VajQ0Bfa9nN9sylhwEXmur7RGKvtGr9tMbGdP9ROOIsHwNd\nngKA34EpjuqsP7DKChlkfuMS9fq+0wzbfsm+ZItpHrxPXWQ5lbPHfxtVcWj1QjW4\nNj2o1p0lTx74IHGRAjN2ABVe5GV/CZ7cPhHI3KGS7MNYPbqs4HzmeSQR54IKEyK7\nZ/xK9S4oIOgifL8tw/2QqnRs/mmMdFN7gYnH4ZONtbBJok5OMd4j+Q==\n=a6B/\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic_wormhole_mailbox_server-0.8.0-py3-none-any.whl.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmoHVeoRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaaeeJgf/RfM1riKqo284TZ3wJJip/74CfGCARX9J\nJRabVxmnwF6ZLEPNCa2QVCdt3QeLNjcFDG8MHKefI+T4Ii0ljwU2V0zE8AKHlsEI\nknH7D8IevrQ3QGFjW5O8N9Ye4OIVIRYnf1JZxgY5Do+2N2hr7jvUrmEHzOXIWtcq\n03LcUeGFOdghhxNpRF7JE6yEJ36n4LSddh6tHTYxCppWxOfLJzWQ2XPReL63b50i\n+wbYCoN1Ihvk3Y+4nZWb5WrBcL4DHGj7CGP5tYQii7jNKVr/g0s6Hv35Gyx6zPsv\n1DE8ZpfaqNonUhXwh1/n0tE++KMrE6sO14434hXiE+KUo9/IHNWPGQ==\n=XsJp\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "signatures/magic_wormhole_mailbox_server-0.8.0.tar.gz.asc",
    "content": "-----BEGIN PGP SIGNATURE-----\n\niQFFBAABCgAvFiEEnVor1WiOy4id680/wmAoAxKAaacFAmoHVeoRHG1lZWphaEBt\nZWVqYWguY2EACgkQwmAoAxKAaaexbQf/dXmzsN34HFn+j5gM+RLp/MavbhzRTspQ\nwYsSHsqfrcSyFREEHNqZGANRVhkmkyprb3oDFYRMzbsBWZwW8am1EsIrkiJAIkog\nebGLQ9Qmihm9bh+7sDnHC1dyXPP1Jgica9zgFc41GlBpa/mLvx5JjmUBK3zypFXi\nrOJMSH7tWiov+sHXNCPcX6xhktAFZR3lWwdVNFfYhqI4g+W2yQd9tC5qKEHTpUJd\nhjcMjLkpvpbwpGUjdY6z6oAUpDoy3S94AAtWbqpwKbOqJr9KBgOltQDKgJBJZ1OY\ntrQY5co4yvsEH3AH92PeZ+X6UnKHRl7kYl97qaak98pZuKlkQKfMYA==\n=lk5u\n-----END PGP SIGNATURE-----\n"
  },
  {
    "path": "src/twisted/plugins/magic_wormhole_mailbox.py",
    "content": "from twisted.application.service import ServiceMaker\n\nMailbox = ServiceMaker(\n    \"Magic-Wormhole Mailbox Server\", # name\n    \"wormhole_mailbox_server.server_tap\", # module\n    \"Provide the Mailbox server for Magic-Wormhole clients.\", # desc\n    \"wormhole-mailbox\", # tapname\n    )\n"
  },
  {
    "path": "src/wormhole_mailbox_server/__init__.py",
    "content": "from . import _version\n__version__ = _version.get_versions()['version']\n"
  },
  {
    "path": "src/wormhole_mailbox_server/_version.py",
    "content": "# This file helps to compute a version number in source trees obtained from\n# git-archive tarball (such as those provided by githubs download-from-tag\n# feature). Distribution tarballs (built by setup.py sdist) and build\n# directories (produced by setup.py build) will contain a much shorter file\n# that just contains the computed version number.\n\n# This file is released into the public domain.\n# Generated by versioneer-0.29\n# https://github.com/python-versioneer/python-versioneer\n\n\"\"\"Git implementation of _version.py.\"\"\"\n\nimport errno\nimport os\nimport re\nimport subprocess\nimport sys\nfrom typing import Any, Callable, Optional\nimport functools\n\n\ndef get_keywords() -> dict[str, str]:\n    \"\"\"Get the keywords needed to look up the version information.\"\"\"\n    # these strings will be replaced by git during git-archive.\n    # setup.py/versioneer.py will grep for the variable names, so they must\n    # each be defined on a line of their own. _version.py will just call\n    # get_keywords().\n    git_refnames = \"$Format:%d$\"\n    git_full = \"$Format:%H$\"\n    git_date = \"$Format:%ci$\"\n    keywords = {\"refnames\": git_refnames, \"full\": git_full, \"date\": git_date}\n    return keywords\n\n\nclass VersioneerConfig:\n    \"\"\"Container for Versioneer configuration parameters.\"\"\"\n\n    VCS: str\n    style: str\n    tag_prefix: str\n    parentdir_prefix: str\n    versionfile_source: str\n    verbose: bool\n\n\ndef get_config() -> VersioneerConfig:\n    \"\"\"Create, populate and return the VersioneerConfig() object.\"\"\"\n    # these strings are filled in when 'setup.py versioneer' creates\n    # _version.py\n    cfg = VersioneerConfig()\n    cfg.VCS = \"git\"\n    cfg.style = \"\"\n    cfg.tag_prefix = \"\"\n    cfg.parentdir_prefix = \"magic-wormhole-mailbox-server\"\n    cfg.versionfile_source = \"src/wormhole_mailbox_server/_version.py\"\n    cfg.verbose = False\n    return cfg\n\n\nclass NotThisMethod(Exception):\n    \"\"\"Exception raised if a method is not valid for the current scenario.\"\"\"\n\n\nLONG_VERSION_PY: dict[str, str] = {}\nHANDLERS: dict[str, dict[str, Callable]] = {}\n\n\ndef register_vcs_handler(vcs: str, method: str) -> Callable:  # decorator\n    \"\"\"Create decorator to mark a method as the handler of a VCS.\"\"\"\n    def decorate(f: Callable) -> Callable:\n        \"\"\"Store f in HANDLERS[vcs][method].\"\"\"\n        if vcs not in HANDLERS:\n            HANDLERS[vcs] = {}\n        HANDLERS[vcs][method] = f\n        return f\n    return decorate\n\n\ndef run_command(\n    commands: list[str],\n    args: list[str],\n    cwd: Optional[str] = None,\n    verbose: bool = False,\n    hide_stderr: bool = False,\n    env: Optional[dict[str, str]] = None,\n) -> tuple[Optional[str], Optional[int]]:\n    \"\"\"Call the given command(s).\"\"\"\n    assert isinstance(commands, list)\n    process = None\n\n    popen_kwargs: dict[str, Any] = {}\n    if sys.platform == \"win32\":\n        # This hides the console window if pythonw.exe is used\n        startupinfo = subprocess.STARTUPINFO()\n        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW\n        popen_kwargs[\"startupinfo\"] = startupinfo\n\n    for command in commands:\n        try:\n            dispcmd = str([command] + args)\n            # remember shell=False, so use git.cmd on windows, not just git\n            process = subprocess.Popen([command] + args, cwd=cwd, env=env,\n                                       stdout=subprocess.PIPE,\n                                       stderr=(subprocess.PIPE if hide_stderr\n                                               else None), **popen_kwargs)\n            break\n        except OSError as e:\n            if e.errno == errno.ENOENT:\n                continue\n            if verbose:\n                print(f\"unable to run {dispcmd}\")\n                print(e)\n            return None, None\n    else:\n        if verbose:\n            print(f\"unable to find command, tried {commands}\")\n        return None, None\n    stdout = process.communicate()[0].strip().decode()\n    if process.returncode != 0:\n        if verbose:\n            print(f\"unable to run {dispcmd} (error)\")\n            print(f\"stdout was {stdout}\")\n        return None, process.returncode\n    return stdout, process.returncode\n\n\ndef versions_from_parentdir(\n    parentdir_prefix: str,\n    root: str,\n    verbose: bool,\n) -> dict[str, Any]:\n    \"\"\"Try to determine the version from the parent directory name.\n\n    Source tarballs conventionally unpack into a directory that includes both\n    the project name and a version string. We will also support searching up\n    two directory levels for an appropriately named parent directory\n    \"\"\"\n    rootdirs = []\n\n    for _ in range(3):\n        dirname = os.path.basename(root)\n        if dirname.startswith(parentdir_prefix):\n            return {\"version\": dirname[len(parentdir_prefix):],\n                    \"full-revisionid\": None,\n                    \"dirty\": False, \"error\": None, \"date\": None}\n        rootdirs.append(root)\n        root = os.path.dirname(root)  # up a level\n\n    if verbose:\n        print(\"Tried directories %s but none started with prefix %s\" %\n              (str(rootdirs), parentdir_prefix))\n    raise NotThisMethod(\"rootdir doesn't start with parentdir_prefix\")\n\n\n@register_vcs_handler(\"git\", \"get_keywords\")\ndef git_get_keywords(versionfile_abs: str) -> dict[str, str]:\n    \"\"\"Extract version information from the given file.\"\"\"\n    # the code embedded in _version.py can just fetch the value of these\n    # keywords. When used from setup.py, we don't want to import _version.py,\n    # so we do it with a regexp instead. This function is not used from\n    # _version.py.\n    keywords: dict[str, str] = {}\n    try:\n        with open(versionfile_abs) as fobj:\n            for line in fobj:\n                if line.strip().startswith(\"git_refnames =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"refnames\"] = mo.group(1)\n                if line.strip().startswith(\"git_full =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"full\"] = mo.group(1)\n                if line.strip().startswith(\"git_date =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"date\"] = mo.group(1)\n    except OSError:\n        pass\n    return keywords\n\n\n@register_vcs_handler(\"git\", \"keywords\")\ndef git_versions_from_keywords(\n    keywords: dict[str, str],\n    tag_prefix: str,\n    verbose: bool,\n) -> dict[str, Any]:\n    \"\"\"Get version information from git keywords.\"\"\"\n    if \"refnames\" not in keywords:\n        raise NotThisMethod(\"Short version file found\")\n    date = keywords.get(\"date\")\n    if date is not None:\n        # Use only the last line.  Previous lines may contain GPG signature\n        # information.\n        date = date.splitlines()[-1]\n\n        # git-2.2.0 added \"%cI\", which expands to an ISO-8601 -compliant\n        # datestamp. However we prefer \"%ci\" (which expands to an \"ISO-8601\n        # -like\" string, which we must then edit to make compliant), because\n        # it's been around since git-1.5.3, and it's too difficult to\n        # discover which version we're using, or to work around using an\n        # older one.\n        date = date.strip().replace(\" \", \"T\", 1).replace(\" \", \"\", 1)\n    refnames = keywords[\"refnames\"].strip()\n    if refnames.startswith(\"$Format\"):\n        if verbose:\n            print(\"keywords are unexpanded, not using\")\n        raise NotThisMethod(\"unexpanded keywords, not a git-archive tarball\")\n    refs = {r.strip() for r in refnames.strip(\"()\").split(\",\")}\n    # starting in git-1.8.3, tags are listed as \"tag: foo-1.0\" instead of\n    # just \"foo-1.0\". If we see a \"tag: \" prefix, prefer those.\n    TAG = \"tag: \"\n    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}\n    if not tags:\n        # Either we're using git < 1.8.3, or there really are no tags. We use\n        # a heuristic: assume all version tags have a digit. The old git %d\n        # expansion behaves like git log --decorate=short and strips out the\n        # refs/heads/ and refs/tags/ prefixes that would let us distinguish\n        # between branches and tags. By ignoring refnames without digits, we\n        # filter out many common branch names like \"release\" and\n        # \"stabilization\", as well as \"HEAD\" and \"master\".\n        tags = {r for r in refs if re.search(r'\\d', r)}\n        if verbose:\n            print(f\"discarding '{','.join(refs - tags)}', no digits\")\n    if verbose:\n        print(f\"likely tags: {','.join(sorted(tags))}\")\n    for ref in sorted(tags):\n        # sorting will prefer e.g. \"2.0\" over \"2.0rc1\"\n        if ref.startswith(tag_prefix):\n            r = ref[len(tag_prefix):]\n            # Filter out refs that exactly match prefix or that don't start\n            # with a number once the prefix is stripped (mostly a concern\n            # when prefix is '')\n            if not re.match(r'\\d', r):\n                continue\n            if verbose:\n                print(f\"picking {r}\")\n            return {\"version\": r,\n                    \"full-revisionid\": keywords[\"full\"].strip(),\n                    \"dirty\": False, \"error\": None,\n                    \"date\": date}\n    # no suitable tags, so version is \"0+unknown\", but full hex is still there\n    if verbose:\n        print(\"no suitable tags, using unknown + full revision id\")\n    return {\"version\": \"0+unknown\",\n            \"full-revisionid\": keywords[\"full\"].strip(),\n            \"dirty\": False, \"error\": \"no suitable tags\", \"date\": None}\n\n\n@register_vcs_handler(\"git\", \"pieces_from_vcs\")\ndef git_pieces_from_vcs(\n    tag_prefix: str,\n    root: str,\n    verbose: bool,\n    runner: Callable = run_command\n) -> dict[str, Any]:\n    \"\"\"Get version from 'git describe' in the root of the source tree.\n\n    This only gets called if the git-archive 'subst' keywords were *not*\n    expanded, and _version.py hasn't already been rewritten with a short\n    version string, meaning we're inside a checked out source tree.\n    \"\"\"\n    GITS = [\"git\"]\n    if sys.platform == \"win32\":\n        GITS = [\"git.cmd\", \"git.exe\"]\n\n    # GIT_DIR can interfere with correct operation of Versioneer.\n    # It may be intended to be passed to the Versioneer-versioned project,\n    # but that should not change where we get our version from.\n    env = os.environ.copy()\n    env.pop(\"GIT_DIR\", None)\n    runner = functools.partial(runner, env=env)\n\n    _, rc = runner(GITS, [\"rev-parse\", \"--git-dir\"], cwd=root,\n                   hide_stderr=not verbose)\n    if rc != 0:\n        if verbose:\n            print(f\"Directory {root} not under git control\")\n        raise NotThisMethod(\"'git rev-parse --git-dir' returned error\")\n\n    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]\n    # if there isn't one, this yields HEX[-dirty] (no NUM)\n    describe_out, rc = runner(GITS, [\n        \"describe\", \"--tags\", \"--dirty\", \"--always\", \"--long\",\n        \"--match\", f\"{tag_prefix}[[:digit:]]*\"\n    ], cwd=root)\n    # --long was added in git-1.5.5\n    if describe_out is None:\n        raise NotThisMethod(\"'git describe' failed\")\n    describe_out = describe_out.strip()\n    full_out, rc = runner(GITS, [\"rev-parse\", \"HEAD\"], cwd=root)\n    if full_out is None:\n        raise NotThisMethod(\"'git rev-parse' failed\")\n    full_out = full_out.strip()\n\n    pieces: dict[str, Any] = {}\n    pieces[\"long\"] = full_out\n    pieces[\"short\"] = full_out[:7]  # maybe improved later\n    pieces[\"error\"] = None\n\n    branch_name, rc = runner(GITS, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n                             cwd=root)\n    # --abbrev-ref was added in git-1.6.3\n    if rc != 0 or branch_name is None:\n        raise NotThisMethod(\"'git rev-parse --abbrev-ref' returned error\")\n    branch_name = branch_name.strip()\n\n    if branch_name == \"HEAD\":\n        # If we aren't exactly on a branch, pick a branch which represents\n        # the current commit. If all else fails, we are on a branchless\n        # commit.\n        branches, rc = runner(GITS, [\"branch\", \"--contains\"], cwd=root)\n        # --contains was added in git-1.5.4\n        if rc != 0 or branches is None:\n            raise NotThisMethod(\"'git branch --contains' returned error\")\n        branches = branches.split(\"\\n\")\n\n        # Remove the first line if we're running detached\n        if \"(\" in branches[0]:\n            branches.pop(0)\n\n        # Strip off the leading \"* \" from the list of branches.\n        branches = [branch[2:] for branch in branches]\n        if \"master\" in branches:\n            branch_name = \"master\"\n        elif not branches:\n            branch_name = None\n        else:\n            # Pick the first branch that is returned. Good or bad.\n            branch_name = branches[0]\n\n    pieces[\"branch\"] = branch_name\n\n    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]\n    # TAG might have hyphens.\n    git_describe = describe_out\n\n    # look for -dirty suffix\n    dirty = git_describe.endswith(\"-dirty\")\n    pieces[\"dirty\"] = dirty\n    if dirty:\n        git_describe = git_describe[:git_describe.rindex(\"-dirty\")]\n\n    # now we have TAG-NUM-gHEX or HEX\n\n    if \"-\" in git_describe:\n        # TAG-NUM-gHEX\n        mo = re.search(r'^(.+)-(\\d+)-g([0-9a-f]+)$', git_describe)\n        if not mo:\n            # unparsable. Maybe git-describe is misbehaving?\n            pieces[\"error\"] = f\"unable to parse git-describe output: '{describe_out}'\"\n            return pieces\n\n        # tag\n        full_tag = mo.group(1)\n        if not full_tag.startswith(tag_prefix):\n            if verbose:\n                fmt = \"tag '%s' doesn't start with prefix '%s'\"\n                print(fmt % (full_tag, tag_prefix))\n            pieces[\"error\"] = (\"tag '%s' doesn't start with prefix '%s'\"\n                               % (full_tag, tag_prefix))\n            return pieces\n        pieces[\"closest-tag\"] = full_tag[len(tag_prefix):]\n\n        # distance: number of commits since tag\n        pieces[\"distance\"] = int(mo.group(2))\n\n        # commit: short hex revision ID\n        pieces[\"short\"] = mo.group(3)\n\n    else:\n        # HEX: no tags\n        pieces[\"closest-tag\"] = None\n        out, rc = runner(GITS, [\"rev-list\", \"HEAD\", \"--left-right\"], cwd=root)\n        pieces[\"distance\"] = len(out.split())  # total number of commits\n\n    # commit date: see ISO-8601 comment in git_versions_from_keywords()\n    date = runner(GITS, [\"show\", \"-s\", \"--format=%ci\", \"HEAD\"], cwd=root)[0].strip()\n    # Use only the last line.  Previous lines may contain GPG signature\n    # information.\n    date = date.splitlines()[-1]\n    pieces[\"date\"] = date.strip().replace(\" \", \"T\", 1).replace(\" \", \"\", 1)\n\n    return pieces\n\n\ndef plus_or_dot(pieces: dict[str, Any]) -> str:\n    \"\"\"Return a + if we don't already have one, else return a .\"\"\"\n    if \"+\" in pieces.get(\"closest-tag\", \"\"):\n        return \".\"\n    return \"+\"\n\n\ndef render_pep440(pieces: dict[str, Any]) -> str:\n    \"\"\"Build up version string, with post-release \"local version identifier\".\n\n    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you\n    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty\n\n    Exceptions:\n    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += plus_or_dot(pieces)\n            rendered += \"%d.g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0+untagged.%d.g%s\" % (pieces[\"distance\"],\n                                          pieces[\"short\"])\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef render_pep440_branch(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .\n\n    The \".dev0\" means not master branch. Note that .dev0 sorts backwards\n    (a feature branch will appear \"older\" than the master branch).\n\n    Exceptions:\n    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            if pieces[\"branch\"] != \"master\":\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += \"%d.g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0\"\n        if pieces[\"branch\"] != \"master\":\n            rendered += \".dev0\"\n        rendered += \"+untagged.%d.g%s\" % (pieces[\"distance\"],\n                                          pieces[\"short\"])\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef pep440_split_post(ver: str) -> tuple[str, Optional[int]]:\n    \"\"\"Split pep440 version string at the post-release segment.\n\n    Returns the release segments before the post-release and the\n    post-release version number (or -1 if no post-release segment is present).\n    \"\"\"\n    vc = str.split(ver, \".post\")\n    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None\n\n\ndef render_pep440_pre(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[.postN.devDISTANCE] -- No -dirty.\n\n    Exceptions:\n    1: no tags. 0.post0.devDISTANCE\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        if pieces[\"distance\"]:\n            # update the post release segment\n            tag_version, post_version = pep440_split_post(pieces[\"closest-tag\"])\n            rendered = tag_version\n            if post_version is not None:\n                rendered += \".post%d.dev%d\" % (post_version + 1, pieces[\"distance\"])\n            else:\n                rendered += \".post0.dev%d\" % (pieces[\"distance\"])\n        else:\n            # no commits, use the tag as the version\n            rendered = pieces[\"closest-tag\"]\n    else:\n        # exception #1\n        rendered = \"0.post0.dev%d\" % pieces[\"distance\"]\n    return rendered\n\n\ndef render_pep440_post(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]+gHEX] .\n\n    The \".dev0\" means dirty. Note that .dev0 sorts backwards\n    (a dirty tree will appear \"older\" than the corresponding clean one),\n    but you shouldn't be releasing software with -dirty anyways.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%d\" % pieces[\"distance\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += f\"g{pieces['short']}\"\n    else:\n        # exception #1\n        rendered = \"0.post%d\" % pieces[\"distance\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dev0\"\n        rendered += f\"+g{pieces['short']}\"\n    return rendered\n\n\ndef render_pep440_post_branch(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .\n\n    The \".dev0\" means not master branch.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%d\" % pieces[\"distance\"]\n            if pieces[\"branch\"] != \"master\":\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += f\"g{pieces['short']}\"\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0.post%d\" % pieces[\"distance\"]\n        if pieces[\"branch\"] != \"master\":\n            rendered += \".dev0\"\n        rendered += f\"+g{pieces['short']}\"\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef render_pep440_old(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]] .\n\n    The \".dev0\" means dirty.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%d\" % pieces[\"distance\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dev0\"\n    else:\n        # exception #1\n        rendered = \"0.post%d\" % pieces[\"distance\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dev0\"\n    return rendered\n\n\ndef render_git_describe(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[-DISTANCE-gHEX][-dirty].\n\n    Like 'git describe --tags --dirty --always'.\n\n    Exceptions:\n    1: no tags. HEX[-dirty]  (note: no 'g' prefix)\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"]:\n            rendered += \"-%d-g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n    else:\n        # exception #1\n        rendered = pieces[\"short\"]\n    if pieces[\"dirty\"]:\n        rendered += \"-dirty\"\n    return rendered\n\n\ndef render_git_describe_long(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG-DISTANCE-gHEX[-dirty].\n\n    Like 'git describe --tags --dirty --always -long'.\n    The distance/hash is unconditional.\n\n    Exceptions:\n    1: no tags. HEX[-dirty]  (note: no 'g' prefix)\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        rendered += \"-%d-g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n    else:\n        # exception #1\n        rendered = pieces[\"short\"]\n    if pieces[\"dirty\"]:\n        rendered += \"-dirty\"\n    return rendered\n\n\ndef render(pieces: dict[str, Any], style: str) -> dict[str, Any]:\n    \"\"\"Render the given version pieces into the requested style.\"\"\"\n    if pieces[\"error\"]:\n        return {\"version\": \"unknown\",\n                \"full-revisionid\": pieces.get(\"long\"),\n                \"dirty\": None,\n                \"error\": pieces[\"error\"],\n                \"date\": None}\n\n    if not style or style == \"default\":\n        style = \"pep440\"  # the default\n\n    if style == \"pep440\":\n        rendered = render_pep440(pieces)\n    elif style == \"pep440-branch\":\n        rendered = render_pep440_branch(pieces)\n    elif style == \"pep440-pre\":\n        rendered = render_pep440_pre(pieces)\n    elif style == \"pep440-post\":\n        rendered = render_pep440_post(pieces)\n    elif style == \"pep440-post-branch\":\n        rendered = render_pep440_post_branch(pieces)\n    elif style == \"pep440-old\":\n        rendered = render_pep440_old(pieces)\n    elif style == \"git-describe\":\n        rendered = render_git_describe(pieces)\n    elif style == \"git-describe-long\":\n        rendered = render_git_describe_long(pieces)\n    else:\n        raise ValueError(f\"unknown style '{style}'\")\n\n    return {\"version\": rendered, \"full-revisionid\": pieces[\"long\"],\n            \"dirty\": pieces[\"dirty\"], \"error\": None,\n            \"date\": pieces.get(\"date\")}\n\n\ndef get_versions() -> dict[str, Any]:\n    \"\"\"Get version information or return default if unable to do so.\"\"\"\n    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have\n    # __file__, we can work backwards from there to the root. Some\n    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which\n    # case we can only use expanded keywords.\n\n    cfg = get_config()\n    verbose = cfg.verbose\n\n    try:\n        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,\n                                          verbose)\n    except NotThisMethod:\n        pass\n\n    try:\n        root = os.path.realpath(__file__)\n        # versionfile_source is the relative path from the top of the source\n        # tree (where the .git directory might live) to this file. Invert\n        # this to find the root from __file__.\n        for _ in cfg.versionfile_source.split('/'):\n            root = os.path.dirname(root)\n    except NameError:\n        return {\"version\": \"0+unknown\", \"full-revisionid\": None,\n                \"dirty\": None,\n                \"error\": \"unable to find root of source tree\",\n                \"date\": None}\n\n    try:\n        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)\n        return render(pieces, cfg.style)\n    except NotThisMethod:\n        pass\n\n    try:\n        if cfg.parentdir_prefix:\n            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)\n    except NotThisMethod:\n        pass\n\n    return {\"version\": \"0+unknown\", \"full-revisionid\": None,\n            \"dirty\": None,\n            \"error\": \"unable to compute version\", \"date\": None}\n"
  },
  {
    "path": "src/wormhole_mailbox_server/database.py",
    "content": "import importlib.resources\nimport os, shutil\nimport sqlite3\nimport tempfile\n\nfrom twisted.python import log\n\nclass DBError(Exception):\n    pass\n\ndef get_schema(name, version):\n    sql_filepath = f\"db-schemas/{name}-v{version}.sql\"\n    path = importlib.resources.files(\"wormhole_mailbox_server\").joinpath(sql_filepath)\n    return path.read_text(encoding=\"utf-8\")\n\ndef get_upgrader(name, new_version):\n    sql_filepath = f\"db-schemas/upgrade-{name}-to-v{new_version}.sql\"\n    path = importlib.resources.files(\"wormhole_mailbox_server\").joinpath(sql_filepath)\n    try:\n        return path.read_text(encoding=\"utf-8\")\n    except OSError: # includes FileNotFoundError\n        raise ValueError(\"no upgrader for %d\" % new_version)\n\n\nCHANNELDB_TARGET_VERSION = 1\nUSAGEDB_TARGET_VERSION = 2\n\ndef dict_factory(cursor, row):\n    d = {}\n    for idx, col in enumerate(cursor.description):\n        d[col[0]] = row[idx]\n    return d\n\ndef _initialize_db_schema(db, name, target_version):\n    \"\"\"Creates the application schema in the given database.\n    \"\"\"\n    log.msg(f\"populating new database with schema {name} v{target_version}\")\n    schema = get_schema(name, target_version)\n    db.executescript(schema)\n    db.execute(\"INSERT INTO version (version) VALUES (?)\",\n               (target_version,))\n    db.commit()\n\ndef _initialize_db_connection(db):\n    \"\"\"Sets up the db connection object with a row factory and with necessary\n    foreign key settings.\n    \"\"\"\n    db.row_factory = dict_factory\n    db.execute(\"PRAGMA foreign_keys = ON\")\n    problems = db.execute(\"PRAGMA foreign_key_check\").fetchall()\n    if problems:\n        raise DBError(f\"failed foreign key check: {problems}\")\n\ndef _open_db_connection(dbfile):\n    \"\"\"Open a new connection to the SQLite3 database at the given path.\n    \"\"\"\n    try:\n        db = sqlite3.connect(dbfile)\n        _initialize_db_connection(db)\n    except (OSError, sqlite3.OperationalError, sqlite3.DatabaseError) as e:\n        # this indicates that the file is not a compatible database format.\n        # Perhaps it was created with an old version, or it might be junk.\n        raise DBError(f\"Unable to create/open db file {dbfile}: {e}\")\n    return db\n\ndef _get_temporary_dbfile(dbfile):\n    \"\"\"Get a temporary filename near the given path.\n    \"\"\"\n    fd, name = tempfile.mkstemp(\n        prefix=os.path.basename(dbfile) + \".\",\n        dir=os.path.dirname(dbfile)\n    )\n    os.close(fd)\n    return name\n\ndef _atomic_create_and_initialize_db(dbfile, name, target_version):\n    \"\"\"Create and return a new database, initialized with the application\n    schema.\n\n    If anything goes wrong, nothing is left at the ``dbfile`` path.\n    \"\"\"\n    temp_dbfile = _get_temporary_dbfile(dbfile)\n    db = _open_db_connection(temp_dbfile)\n    _initialize_db_schema(db, name, target_version)\n    db.close()\n    os.rename(temp_dbfile, dbfile)\n    return _open_db_connection(dbfile)\n\ndef _get_db(dbfile, name, target_version):\n    \"\"\"Open or create the given db file. The parent directory must exist.\n    Returns the db connection object, or raises DBError.\n    \"\"\"\n    if dbfile == \":memory:\":\n        db = _open_db_connection(dbfile)\n        _initialize_db_schema(db, name, target_version)\n    elif os.path.exists(dbfile):\n        db = _open_db_connection(dbfile)\n    else:\n        db = _atomic_create_and_initialize_db(dbfile, name, target_version)\n\n    version = db.execute(\"SELECT version FROM version\").fetchone()[\"version\"]\n\n    if version < target_version and dbfile != \":memory:\":\n        backup_fn = \"%s-backup-v%d\" % (dbfile, version)\n        log.msg(\" storing backup of v%d db in %s\" % (version, backup_fn))\n        shutil.copy(dbfile, backup_fn)\n\n    while version < target_version:\n        log.msg(f\" need to upgrade from {version} to {target_version}\")\n        try:\n            upgrader = get_upgrader(name, version+1)\n        except ValueError:\n            log.msg(f\" unable to upgrade {version} to {version + 1}\")\n            raise DBError(\"Unable to upgrade %s to version %s, left at %s\"\n                          % (dbfile, version+1, version))\n        log.msg(f\" executing upgrader v{version}->v{version + 1}\")\n        db.executescript(upgrader)\n        db.commit()\n        version = version+1\n\n    if version != target_version:\n        raise DBError(f\"Unable to handle db version {version}\")\n\n    return db\n\ndef create_or_upgrade_channel_db(dbfile):\n    return _get_db(dbfile, \"channel\", CHANNELDB_TARGET_VERSION)\n\ndef create_or_upgrade_usage_db(dbfile):\n    if dbfile is None:\n        return None\n    return _get_db(dbfile, \"usage\", USAGEDB_TARGET_VERSION)\n\nclass DBDoesntExist(Exception):\n    pass\n\ndef open_existing_db(dbfile):\n    assert dbfile != \":memory:\"\n    if not os.path.exists(dbfile):\n        raise DBDoesntExist()\n    return _open_db_connection(dbfile)\n\nclass DBAlreadyExists(Exception):\n    pass\n\ndef create_channel_db(dbfile):\n    \"\"\"Create the given db file. Refuse to touch a pre-existing file.\n\n    This is meant for use by migration tools, to create the output target\"\"\"\n\n    if dbfile == \":memory:\":\n        db = _open_db_connection(dbfile)\n        _initialize_db_schema(db, \"channel\", CHANNELDB_TARGET_VERSION)\n    elif os.path.exists(dbfile):\n        raise DBAlreadyExists()\n    else:\n        db = _atomic_create_and_initialize_db(dbfile, \"channel\",\n                                              CHANNELDB_TARGET_VERSION)\n    return db\n\ndef create_usage_db(dbfile):\n    if dbfile == \":memory:\":\n        db = _open_db_connection(dbfile)\n        _initialize_db_schema(db, \"usage\", USAGEDB_TARGET_VERSION)\n    elif os.path.exists(dbfile):\n        raise DBAlreadyExists()\n    else:\n        db = _atomic_create_and_initialize_db(dbfile, \"usage\",\n                                              USAGEDB_TARGET_VERSION)\n    return db\n\ndef dump_db(db):\n    # to let _iterdump work, we need to restore the original row factory\n    orig = db.row_factory\n    try:\n        db.row_factory = sqlite3.Row\n        return \"\".join(db.iterdump())\n    finally:\n        db.row_factory = orig\n"
  },
  {
    "path": "src/wormhole_mailbox_server/db-schemas/channel-v1.sql",
    "content": "\n-- note: anything which isn't an boolean, integer, or human-readable unicode\n-- string, (i.e. binary strings) will be stored as hex\n\nCREATE TABLE `version`\n(\n `version` INTEGER -- contains one row, set to 1\n);\n\n\n-- Wormhole codes use a \"nameplate\": a short name which is only used to\n-- reference a specific (long-named) mailbox. The codes only use numeric\n-- nameplates, but the protocol and server allow can use arbitrary strings.\nCREATE TABLE `nameplates`\n(\n `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n `app_id` VARCHAR,\n `name` VARCHAR,\n `mailbox_id` VARCHAR REFERENCES `mailboxes`(`id`),\n `request_id` VARCHAR -- from 'allocate' message, for future deduplication\n);\nCREATE INDEX `nameplates_idx` ON `nameplates` (`app_id`, `name`);\nCREATE INDEX `nameplates_mailbox_idx` ON `nameplates` (`app_id`, `mailbox_id`);\nCREATE INDEX `nameplates_request_idx` ON `nameplates` (`app_id`, `request_id`);\n\nCREATE TABLE `nameplate_sides`\n(\n `nameplates_id` REFERENCES `nameplates`(`id`),\n `claimed` BOOLEAN, -- True after claim(), False after release()\n `side` VARCHAR,\n `added` INTEGER -- time when this side first claimed the nameplate\n);\n\n\n-- Clients exchange messages through a \"mailbox\", which has a long (randomly\n-- unique) identifier and a queue of messages.\n-- `id` is randomly-generated and unique across all apps.\nCREATE TABLE `mailboxes`\n(\n `app_id` VARCHAR,\n `id` VARCHAR PRIMARY KEY,\n `updated` INTEGER, -- time of last activity, used for pruning\n `for_nameplate` BOOLEAN -- allocated for a nameplate, not standalone\n);\nCREATE INDEX `mailboxes_idx` ON `mailboxes` (`app_id`, `id`);\n\nCREATE TABLE `mailbox_sides`\n(\n `mailbox_id` REFERENCES `mailboxes`(`id`),\n `opened` BOOLEAN, -- True after open(), False after close()\n `side` VARCHAR,\n `added` INTEGER, -- time when this side first opened the mailbox\n `mood` VARCHAR\n);\n\nCREATE TABLE `messages`\n(\n `app_id` VARCHAR,\n `mailbox_id` VARCHAR,\n `side` VARCHAR,\n `phase` VARCHAR, -- numeric or string\n `body` VARCHAR,\n `server_rx` INTEGER,\n `msg_id` VARCHAR\n);\nCREATE INDEX `messages_idx` ON `messages` (`app_id`, `mailbox_id`);\n"
  },
  {
    "path": "src/wormhole_mailbox_server/db-schemas/upgrade-usage-to-v2.sql",
    "content": "CREATE TABLE `client_versions`\n(\n `app_id` VARCHAR,\n `side` VARCHAR, -- for deduplication of reconnects\n `connect_time` INTEGER, -- seconds since epoch, rounded to \"blur time\"\n -- the client sends us a 'client_version' tuple of (implementation, version)\n -- the Python client sends e.g. (\"python\", \"0.11.0\")\n `implementation` VARCHAR,\n `version` VARCHAR\n);\nCREATE INDEX `client_versions_time_idx` on `client_versions` (`connect_time`);\nCREATE INDEX `client_versions_appid_time_idx` on `client_versions` (`app_id`, `connect_time`);\n\nDELETE FROM `version`;\nINSERT INTO `version` (`version`) VALUES (2);\n"
  },
  {
    "path": "src/wormhole_mailbox_server/db-schemas/usage-v1.sql",
    "content": "CREATE TABLE `version`\n(\n `version` INTEGER -- contains one row\n);\n\nCREATE TABLE `current`\n(\n `rebooted` INTEGER, -- seconds since epoch of most recent reboot\n `updated` INTEGER, -- when `current` was last updated\n `blur_time` INTEGER, -- `started` is rounded to this, or None\n `connections_websocket` INTEGER -- number of live clients via websocket\n);\n\n-- one row is created each time a nameplate is retired\nCREATE TABLE `nameplates`\n(\n `app_id` VARCHAR,\n `started` INTEGER, -- seconds since epoch, rounded to \"blur time\"\n `waiting_time` INTEGER, -- seconds from start to 2nd side appearing, or None\n `total_time` INTEGER, -- seconds from open to last close/prune\n `result` VARCHAR -- happy, lonely, pruney, crowded\n -- nameplate moods:\n --  \"happy\": two sides open and close\n --  \"lonely\": one side opens and closes (no response from 2nd side)\n --  \"pruney\": channels which get pruned for inactivity\n --  \"crowded\": three or more sides were involved\n);\nCREATE INDEX `nameplates_idx` ON `nameplates` (`app_id`, `started`);\n\n-- one row is created each time a mailbox is retired\nCREATE TABLE `mailboxes`\n(\n `app_id` VARCHAR,\n `for_nameplate` BOOLEAN, -- allocated for a nameplate, not standalone\n `started` INTEGER, -- seconds since epoch, rounded to \"blur time\"\n `total_time` INTEGER, -- seconds from open to last close\n `waiting_time` INTEGER, -- seconds from start to 2nd side appearing, or None\n `result` VARCHAR -- happy, scary, lonely, errory, pruney\n -- rendezvous moods:\n --  \"happy\": both sides close with mood=happy\n --  \"scary\": any side closes with mood=scary (bad MAC, probably wrong pw)\n --  \"lonely\": any side closes with mood=lonely (no response from 2nd side)\n --  \"errory\": any side closes with mood=errory (other errors)\n --  \"pruney\": channels which get pruned for inactivity\n --  \"crowded\": three or more sides were involved\n);\nCREATE INDEX `mailboxes_idx` ON `mailboxes` (`app_id`, `started`);\nCREATE INDEX `mailboxes_result_idx` ON `mailboxes` (`result`);\n"
  },
  {
    "path": "src/wormhole_mailbox_server/db-schemas/usage-v2.sql",
    "content": "CREATE TABLE `version`\n(\n `version` INTEGER -- contains one row\n);\n\nCREATE TABLE `current`\n(\n `rebooted` INTEGER, -- seconds since epoch of most recent reboot\n `updated` INTEGER, -- when `current` was last updated\n `blur_time` INTEGER, -- `started` is rounded to this, or None\n `connections_websocket` INTEGER -- number of live clients via websocket\n);\n\n-- one row is created each time a nameplate is retired\nCREATE TABLE `nameplates`\n(\n `app_id` VARCHAR,\n `started` INTEGER, -- seconds since epoch, rounded to \"blur time\"\n `waiting_time` INTEGER, -- seconds from start to 2nd side appearing, or None\n `total_time` INTEGER, -- seconds from open to last close/prune\n `result` VARCHAR -- happy, lonely, pruney, crowded\n -- nameplate moods:\n --  \"happy\": two sides open and close\n --  \"lonely\": one side opens and closes (no response from 2nd side)\n --  \"pruney\": channels which get pruned for inactivity\n --  \"crowded\": three or more sides were involved\n);\nCREATE INDEX `nameplates_idx` ON `nameplates` (`app_id`, `started`);\n\n-- one row is created each time a mailbox is retired\nCREATE TABLE `mailboxes`\n(\n `app_id` VARCHAR,\n `for_nameplate` BOOLEAN, -- allocated for a nameplate, not standalone\n `started` INTEGER, -- seconds since epoch, rounded to \"blur time\"\n `total_time` INTEGER, -- seconds from open to last close\n `waiting_time` INTEGER, -- seconds from start to 2nd side appearing, or None\n `result` VARCHAR -- happy, scary, lonely, errory, pruney\n -- rendezvous moods:\n --  \"happy\": both sides close with mood=happy\n --  \"scary\": any side closes with mood=scary (bad MAC, probably wrong pw)\n --  \"lonely\": any side closes with mood=lonely (no response from 2nd side)\n --  \"errory\": any side closes with mood=errory (other errors)\n --  \"pruney\": channels which get pruned for inactivity\n --  \"crowded\": three or more sides were involved\n);\nCREATE INDEX `mailboxes_idx` ON `mailboxes` (`app_id`, `started`);\nCREATE INDEX `mailboxes_result_idx` ON `mailboxes` (`result`);\n\nCREATE TABLE `client_versions`\n(\n `app_id` VARCHAR,\n `side` VARCHAR, -- for deduplication of reconnects\n `connect_time` INTEGER, -- seconds since epoch, rounded to \"blur time\"\n -- the client sends us a 'client_version' tuple of (implementation, version)\n -- the Python client sends e.g. (\"python\", \"0.11.0\")\n `implementation` VARCHAR,\n `version` VARCHAR\n);\nCREATE INDEX `client_versions_time_idx` on `client_versions` (`connect_time`);\nCREATE INDEX `client_versions_appid_time_idx` on `client_versions` (`app_id`, `connect_time`);\n"
  },
  {
    "path": "src/wormhole_mailbox_server/increase_rlimits.py",
    "content": "try:\n    # 'resource' is unix-only\n    from resource import getrlimit, setrlimit, RLIMIT_NOFILE\nexcept ImportError: # pragma: nocover\n    getrlimit, setrlimit, RLIMIT_NOFILE = None, None, None # pragma: nocover\nfrom twisted.python import log\n\ndef increase_rlimits():\n    if getrlimit is None:\n        log.msg(\"unable to import 'resource', leaving rlimit alone\")\n        return\n    soft, hard = getrlimit(RLIMIT_NOFILE)\n    if soft >= 10000:\n        log.msg(\"RLIMIT_NOFILE.soft was %d, leaving it alone\" % soft)\n        return\n    # OS-X defaults to soft=7168, and reports a huge number for 'hard',\n    # but won't accept anything more than soft=10240, so we can't just\n    # set soft=hard. Linux returns (1024, 1048576) and is fine with\n    # soft=hard. Cygwin is reported to return (256,-1) and accepts up to\n    # soft=3200. So we try multiple values until something works.\n    for newlimit in [hard, 10000, 3200, 1024]:\n        log.msg(f\"changing RLIMIT_NOFILE from ({soft},{hard}) to ({newlimit},{hard})\")\n        try:\n            setrlimit(RLIMIT_NOFILE, (newlimit, hard))\n            log.msg(\"setrlimit successful\")\n            return\n        except ValueError as e:\n            log.msg(f\"error during setrlimit: {e}\")\n            continue\n        except:\n            log.msg(\"other error during setrlimit, leaving it alone\")\n            log.err()\n            return\n    log.msg(\"unable to change rlimit, leaving it alone\")\n"
  },
  {
    "path": "src/wormhole_mailbox_server/server.py",
    "content": "import os, random, base64, re\nfrom collections import namedtuple\nfrom twisted.python import log\nfrom twisted.application import service\n\ndef generate_mailbox_id():\n    return base64.b32encode(os.urandom(8)).lower().strip(b\"=\").decode(\"ascii\")\n\nNAMEPLATE_RE = re.compile(r'^\\d+$')\n\ndef check_valid_nameplate(n):\n    if not isinstance(n, str):\n        raise ValueError(\"nameplate %r is %s not str\" % (n, type(n)))\n    if len(n) > 40:\n        raise ValueError(\"nameplate %s .. is too long, %d>40\" % (repr(n)[:50], len(n)))\n    if not NAMEPLATE_RE.search(n):\n        raise ValueError(\"nameplate %s has non-digits\" % (n,))\n\nclass CrowdedError(Exception):\n    pass\nclass ReclaimedError(Exception):\n    pass\n\nUsage = namedtuple(\"Usage\", [\"started\", \"waiting_time\", \"total_time\", \"result\"])\nTransitUsage = namedtuple(\"TransitUsage\",\n                          [\"started\", \"waiting_time\", \"total_time\",\n                           \"total_bytes\", \"result\"])\n\nSidedMessage = namedtuple(\"SidedMessage\", [\"side\", \"phase\", \"body\",\n                                           \"server_rx\", \"msg_id\"])\n\nclass Mailbox:\n    def __init__(self, app, db, usage_db, app_id, mailbox_id):\n        self._app = app\n        self._db = db\n        self._usage_db = usage_db\n        self._app_id = app_id\n        self._mailbox_id = mailbox_id\n        self._listeners = {} # handle -> (send_f, stop_f)\n        # \"handle\" is a hashable object, for deregistration\n        # send_f() takes a JSONable object, stop_f() has no args\n\n    def open(self, side, when):\n        # requires caller to db.commit()\n        assert isinstance(side, str), type(side)\n        db = self._db\n\n        already = db.execute(\"SELECT * FROM `mailbox_sides`\"\n                             \" WHERE `mailbox_id`=? AND `side`=?\",\n                             (self._mailbox_id, side)).fetchone()\n        if not already:\n            db.execute(\"INSERT INTO `mailbox_sides`\"\n                       \" (`mailbox_id`, `opened`, `side`, `added`)\"\n                       \" VALUES(?,?,?,?)\",\n                       (self._mailbox_id, True, side, when))\n        # We accept re-opening a mailbox which a side previously closed,\n        # unlike claim_nameplate(), which forbids any side from re-claiming a\n        # nameplate which they previously released. (Nameplates forbid this\n        # because the act of claiming a nameplate for the first time causes a\n        # new mailbox to be created, which should only happen once).\n        # Mailboxes have their own distinct objects (to manage\n        # subscriptions), so closing one which was already closed requires\n        # making a new object, which works by calling open() just before\n        # close(). We really do want to support re-closing closed mailboxes,\n        # because this enables intermittently-connected clients, who remember\n        # sending a 'close' but aren't sure whether it was received or not,\n        # then get shut down. Those clients will wake up and re-send the\n        # 'close', until they receive the 'closed' ack message.\n\n        self._touch(when)\n        db.commit() # XXX: reconcile the need for this with the comment above\n\n    def _touch(self, when):\n        self._db.execute(\"UPDATE `mailboxes` SET `updated`=? WHERE `id`=?\",\n                         (when, self._mailbox_id))\n\n    def get_messages(self):\n        messages = []\n        db = self._db\n        for row in db.execute(\"SELECT * FROM `messages`\"\n                              \" WHERE `app_id`=? AND `mailbox_id`=?\"\n                              \" ORDER BY `server_rx` ASC\",\n                              (self._app_id, self._mailbox_id)).fetchall():\n            sm = SidedMessage(side=row[\"side\"], phase=row[\"phase\"],\n                              body=row[\"body\"], server_rx=row[\"server_rx\"],\n                              msg_id=row[\"msg_id\"])\n            messages.append(sm)\n        return messages\n\n    def add_listener(self, handle, send_f, stop_f):\n        #log.msg(\"add_listener\", self._mailbox_id, handle)\n        self._listeners[handle] = (send_f, stop_f)\n        #log.msg(\" added\", len(self._listeners))\n        return self.get_messages()\n\n    def remove_listener(self, handle):\n        #log.msg(\"remove_listener\", self._mailbox_id, handle)\n        self._listeners.pop(handle, None)\n        #log.msg(\" removed\", len(self._listeners))\n\n    def has_listeners(self):\n        return bool(self._listeners)\n\n    def count_listeners(self):\n        return len(self._listeners)\n\n    def broadcast_message(self, sm):\n        for (send_f, stop_f) in self._listeners.values():\n            send_f(sm)\n\n    def _add_message(self, sm):\n        self._db.execute(\"INSERT INTO `messages`\"\n                         \" (`app_id`, `mailbox_id`, `side`, `phase`,  `body`,\"\n                         \"  `server_rx`, `msg_id`)\"\n                         \" VALUES (?,?,?,?,?, ?,?)\",\n                         (self._app_id, self._mailbox_id, sm.side,\n                          sm.phase, sm.body, sm.server_rx, sm.msg_id))\n        self._touch(sm.server_rx)\n        self._db.commit()\n\n    def add_message(self, sm):\n        assert isinstance(sm, SidedMessage)\n        self._add_message(sm)\n        self.broadcast_message(sm)\n\n    def close(self, side, mood, when):\n        assert isinstance(side, str), type(side)\n        db = self._db\n        row = db.execute(\"SELECT * FROM `mailboxes`\"\n                         \" WHERE `app_id`=? AND `id`=?\",\n                         (self._app_id, self._mailbox_id)).fetchone()\n        if not row:\n            return\n        for_nameplate = row[\"for_nameplate\"]\n\n        row = db.execute(\"SELECT * FROM `mailbox_sides`\"\n                         \" WHERE `mailbox_id`=? AND `side`=?\",\n                         (self._mailbox_id, side)).fetchone()\n        if not row:\n            return\n        db.execute(\"UPDATE `mailbox_sides` SET `opened`=?, `mood`=?\"\n                   \" WHERE `mailbox_id`=? AND `side`=?\",\n                   (False, mood, self._mailbox_id, side))\n        db.commit()\n\n        # are any sides still open?\n        side_rows = db.execute(\"SELECT * FROM `mailbox_sides`\"\n                               \" WHERE `mailbox_id`=?\",\n                               (self._mailbox_id,)).fetchall()\n        if any([sr[\"opened\"] for sr in side_rows]):\n            return\n\n        # nope. delete and summarize\n\n        # if the nameplate is still allocated we'll get a foreign-key\n        # failure when trying to delete the mailbox, so get rid of\n        # those first\n        db.execute(\"DELETE FROM `nameplate_sides` WHERE `side`=?\",\n                   (side,))\n        db.execute(\"DELETE FROM `nameplates` WHERE `mailbox_id`=?\",\n                   (self._mailbox_id,))\n        # remove mailbox content\n        db.execute(\"DELETE FROM `messages` WHERE `mailbox_id`=?\",\n                   (self._mailbox_id,))\n        db.execute(\"DELETE FROM `mailbox_sides` WHERE `mailbox_id`=?\",\n                   (self._mailbox_id,))\n        db.execute(\"DELETE FROM `mailboxes` WHERE `id`=?\", (self._mailbox_id,))\n        if self._usage_db:\n            self._app._summarize_mailbox_and_store(for_nameplate, side_rows,\n                                                when, pruned=False)\n            self._usage_db.commit()\n        db.commit()\n        # Shut down any listeners, just in case they're still lingering\n        # around.\n        for (send_f, stop_f) in self._listeners.values():\n            stop_f()\n        self._listeners = {}\n        self._app.free_mailbox(self._mailbox_id)\n\n    def _shutdown(self):\n        # used at test shutdown to accelerate client disconnects\n        for (send_f, stop_f) in self._listeners.values():\n            stop_f()\n        self._listeners = {}\n\n\nclass AppNamespace:\n\n    def __init__(self, db, usage_db, blur_usage, log_requests, app_id,\n                 allow_list):\n        self._db = db\n        self._usage_db = usage_db\n        self._blur_usage = blur_usage\n        self._log_requests = log_requests\n        self._app_id = app_id\n        self._mailboxes = {}\n        self._allow_list = allow_list\n\n    def log_client_version(self, server_rx, side, client_version):\n        if self._blur_usage:\n            server_rx = self._blur_usage * (server_rx // self._blur_usage)\n        implementation = client_version[0]\n        version = client_version[1]\n        if self._usage_db:\n            self._usage_db.execute(\"INSERT INTO `client_versions`\"\n                                   \" (`app_id`, `side`, `connect_time`,\"\n                                   \"  `implementation`, `version`)\"\n                                   \" VALUES(?,?,?,?,?)\",\n                                   (self._app_id, side, server_rx,\n                                    implementation, version))\n            self._usage_db.commit()\n\n    def get_nameplate_ids(self):\n        if not self._allow_list:\n            return []\n        return self._get_nameplate_ids()\n\n    def _get_nameplate_ids(self):\n        db = self._db\n        # TODO: filter this to numeric ids?\n        c = db.execute(\"SELECT DISTINCT `name` FROM `nameplates`\"\n                       \" WHERE `app_id`=?\", (self._app_id,))\n        return {row[\"name\"] for row in c.fetchall()}\n\n    def _find_available_nameplate_id(self):\n        claimed = self._get_nameplate_ids()\n        for size in range(1,4): # stick to 1-999 for now\n            available = set()\n            for id_int in range(10**(size-1), 10**size):\n                id = \"%d\" % id_int\n                if id not in claimed:\n                    available.add(id)\n            if available:\n                return random.choice(list(available))\n        # ouch, 999 currently claimed. Try random ones for a while.\n        for tries in range(1000):\n            id_int = random.randrange(1000, 1000*1000)\n            id = \"%d\" % id_int\n            if id not in claimed:\n                return id\n        raise ValueError(\"unable to find a free nameplate-id\")\n\n    def allocate_nameplate(self, side, when):\n        nameplate_id = self._find_available_nameplate_id()\n        mailbox_id = self.claim_nameplate(nameplate_id, side, when)\n        del mailbox_id # ignored, they'll learn it from claim()\n        return nameplate_id\n\n    def claim_nameplate(self, name, side, when):\n        # when we're done:\n        # * there will be one row for the nameplate\n        #  * there will be one 'side' attached to it, with claimed=True\n        # * a mailbox id and mailbox row will be created\n        #  * a mailbox 'side' will be attached, with opened=True\n        assert isinstance(name, str), type(name)\n        assert isinstance(side, str), type(side)\n        check_valid_nameplate(name)\n        db = self._db\n        row = db.execute(\"SELECT * FROM `nameplates`\"\n                         \" WHERE `app_id`=? AND `name`=?\",\n                         (self._app_id, name)).fetchone()\n        if not row:\n            if self._log_requests:\n                log.msg(f\"creating nameplate#{name} for app_id {self._app_id}\")\n            mailbox_id = generate_mailbox_id()\n            self._add_mailbox(mailbox_id, True, side, when) # ensure row exists\n            sql = (\"INSERT INTO `nameplates`\"\n                   \" (`app_id`, `name`, `mailbox_id`)\"\n                   \" VALUES(?,?,?)\")\n            npid = db.execute(sql, (self._app_id, name, mailbox_id)\n                              ).lastrowid\n        else:\n            npid = row[\"id\"]\n            mailbox_id = row[\"mailbox_id\"]\n\n        row = db.execute(\"SELECT * FROM `nameplate_sides`\"\n                         \" WHERE `nameplates_id`=? AND `side`=?\",\n                         (npid, side)).fetchone()\n        if not row:\n            db.execute(\"INSERT INTO `nameplate_sides`\"\n                       \" (`nameplates_id`, `claimed`, `side`, `added`)\"\n                       \" VALUES(?,?,?,?)\",\n                       (npid, True, side, when))\n        else:\n            if not row[\"claimed\"]:\n                raise ReclaimedError(\"you cannot re-claim a nameplate that your side previously released\")\n            # since that might cause a new mailbox to be allocated\n        db.commit()\n\n        self.open_mailbox(mailbox_id, side, when) # may raise CrowdedError\n        rows = db.execute(\"SELECT * FROM `nameplate_sides`\"\n                          \" WHERE `nameplates_id`=?\", (npid,)).fetchall()\n        if len(rows) > 2:\n            # this line will probably never get hit: any crowding is noticed\n            # on mailbox_sides first, inside open_mailbox()\n            raise CrowdedError(\"too many sides have claimed this nameplate\")\n        return mailbox_id\n\n    def release_nameplate(self, name, side, when):\n        # when we're done:\n        # * the 'claimed' flag will be cleared on the nameplate_sides row\n        # * if the nameplate is now unused (no claimed sides):\n        #  * a usage record will be added\n        #  * the nameplate row will be removed\n        #  * the nameplate sides will be removed\n        assert isinstance(name, str), type(name)\n        assert isinstance(side, str), type(side)\n        db = self._db\n        np_row = db.execute(\"SELECT * FROM `nameplates`\"\n                            \" WHERE `app_id`=? AND `name`=?\",\n                            (self._app_id, name)).fetchone()\n        if not np_row:\n            return\n        npid = np_row[\"id\"]\n        row = db.execute(\"SELECT * FROM `nameplate_sides`\"\n                         \" WHERE `nameplates_id`=? AND `side`=?\",\n                         (npid, side)).fetchone()\n        if not row:\n            return\n        db.execute(\"UPDATE `nameplate_sides` SET `claimed`=?\"\n                   \" WHERE `nameplates_id`=? AND `side`=?\",\n                   (False, npid, side))\n        db.commit()\n\n        # now, are there any remaining claims?\n        side_rows = db.execute(\"SELECT * FROM `nameplate_sides`\"\n                               \" WHERE `nameplates_id`=?\",\n                               (npid,)).fetchall()\n        claims = [1 for sr in side_rows if sr[\"claimed\"]]\n        if claims:\n            return\n        # delete and summarize\n        db.execute(\"DELETE FROM `nameplate_sides` WHERE `nameplates_id`=?\",\n                   (npid,))\n        db.execute(\"DELETE FROM `nameplates` WHERE `id`=?\", (npid,))\n        if self._usage_db:\n            self._summarize_nameplate_and_store(side_rows, when, pruned=False)\n            self._usage_db.commit()\n        db.commit()\n\n    def _summarize_nameplate_and_store(self, side_rows, delete_time, pruned):\n        # requires caller to self._usage_db.commit()\n        u = self._summarize_nameplate_usage(side_rows, delete_time, pruned)\n        self._usage_db.execute(\"INSERT INTO `nameplates`\"\n                            \" (`app_id`,\"\n                            \" `started`, `total_time`, `waiting_time`, `result`)\"\n                            \" VALUES (?, ?,?,?,?)\",\n                            (self._app_id,\n                            u.started, u.total_time, u.waiting_time, u.result))\n\n    def _summarize_nameplate_usage(self, side_rows, delete_time, pruned):\n        times = sorted([row[\"added\"] for row in side_rows])\n        started = times[0]\n        if self._blur_usage:\n            started = self._blur_usage * (started // self._blur_usage)\n        waiting_time = None\n        if len(times) > 1:\n            waiting_time = times[1] - times[0]\n        total_time = delete_time - times[0]\n        result = \"lonely\"\n        if len(times) == 2:\n            result = \"happy\"\n        if pruned:\n            result = \"pruney\"\n        if len(times) > 2:\n            result = \"crowded\"\n        return Usage(started=started, waiting_time=waiting_time,\n                     total_time=total_time, result=result)\n\n    def _add_mailbox(self, mailbox_id, for_nameplate, side, when):\n        assert isinstance(mailbox_id, str), type(mailbox_id)\n        db = self._db\n        row = db.execute(\"SELECT * FROM `mailboxes`\"\n                         \" WHERE `app_id`=? AND `id`=?\",\n                         (self._app_id, mailbox_id)).fetchone()\n        if not row:\n            self._db.execute(\"INSERT INTO `mailboxes`\"\n                             \" (`app_id`, `id`, `for_nameplate`, `updated`)\"\n                             \" VALUES(?,?,?,?)\",\n                             (self._app_id, mailbox_id, for_nameplate, when))\n            # we don't need a commit here, because mailbox.open() only\n            # does SELECT FROM `mailbox_sides`, not from `mailboxes`\n\n    def open_mailbox(self, mailbox_id, side, when):\n        assert isinstance(mailbox_id, str), type(mailbox_id)\n        self._add_mailbox(mailbox_id, False, side, when) # ensure row exists\n        db = self._db\n        if not mailbox_id in self._mailboxes: # ensure Mailbox object exists\n            if self._log_requests:\n                log.msg(f\"spawning #{mailbox_id} for app_id {self._app_id}\")\n            self._mailboxes[mailbox_id] = Mailbox(self,\n                                                  self._db, self._usage_db,\n                                                  self._app_id, mailbox_id)\n        mailbox = self._mailboxes[mailbox_id]\n\n        # delegate to mailbox.open() to add a row to mailbox_sides, and\n        # update the mailbox.updated timestamp\n        mailbox.open(side, when)\n        db.commit()\n        rows = db.execute(\"SELECT * FROM `mailbox_sides`\"\n                          \" WHERE `mailbox_id`=?\",\n                          (mailbox_id,)).fetchall()\n        if len(rows) > 2:\n            raise CrowdedError(\"too many sides have opened this mailbox\")\n        return mailbox\n\n    def free_mailbox(self, mailbox_id):\n        # called from Mailbox.delete_and_summarize(), which deletes any\n        # messages\n\n        if mailbox_id in self._mailboxes:\n            self._mailboxes.pop(mailbox_id)\n        #if self._log_requests:\n        #    log.msg(\"freed+killed #%s, now have %d DB mailboxes, %d live\" %\n        #            (mailbox_id, len(self.get_claimed()), len(self._mailboxes)))\n\n    def _summarize_mailbox_and_store(self, for_nameplate, side_rows,\n                                     delete_time, pruned):\n        db = self._usage_db\n        u = self._summarize_mailbox(side_rows, delete_time, pruned)\n        db.execute(\"INSERT INTO `mailboxes`\"\n                   \" (`app_id`, `for_nameplate`,\"\n                   \"  `started`, `total_time`, `waiting_time`, `result`)\"\n                   \" VALUES (?,?, ?,?,?,?)\",\n                   (self._app_id, for_nameplate,\n                    u.started, u.total_time, u.waiting_time, u.result))\n\n    def _summarize_mailbox(self, side_rows, delete_time, pruned):\n        times = sorted([row[\"added\"] for row in side_rows])\n        started = times[0]\n        if self._blur_usage:\n            started = self._blur_usage * (started // self._blur_usage)\n        waiting_time = None\n        if len(times) > 1:\n            waiting_time = times[1] - times[0]\n        total_time = delete_time - times[0]\n\n        num_sides = len(times)\n        if num_sides == 0:\n            result = \"quiet\"\n        elif num_sides == 1:\n            result = \"lonely\"\n        else:\n            result = \"happy\"\n\n        # \"mood\" is only recorded at close()\n        moods = [row[\"mood\"] for row in side_rows if row.get(\"mood\")]\n        if \"lonely\" in moods:\n            result = \"lonely\"\n        if \"errory\" in moods:\n            result = \"errory\"\n        if \"scary\" in moods:\n            result = \"scary\"\n        if pruned:\n            result = \"pruney\"\n        if num_sides > 2:\n            result = \"crowded\"\n\n        return Usage(started=started, waiting_time=waiting_time,\n                     total_time=total_time, result=result)\n\n    def prune(self, now, old):\n        # The pruning check runs every 10 minutes, and \"old\" is defined to be\n        # 11 minutes ago (unit tests can use different values). The client is\n        # allowed to disconnect for up to 9 minutes without losing the\n        # channel (nameplate, mailbox, and messages).\n\n        # Each time a client does something, the mailbox.updated field is\n        # updated with the current timestamp. If a client is subscribed to\n        # the mailbox when pruning check runs, the \"updated\" field is also\n        # updated. After that check, if the \"updated\" field is \"old\", the\n        # channel is deleted.\n\n        # For now, pruning is logged even if log_requests is False, to debug\n        # the pruning process, and since pruning is triggered by a timer\n        # instead of by user action. It does reveal which mailboxes were\n        # present when the pruning process began, though, so in the log run\n        # it should do less logging.\n        log.msg(f\" prune begins ({self._app_id})\")\n        db = self._db\n        modified = False\n\n        for mailbox in self._mailboxes.values():\n            if mailbox.has_listeners():\n                log.msg(f\"touch {mailbox._mailbox_id} because listeners\")\n                mailbox._touch(now)\n        db.commit() # make sure the updates are visible below\n\n        new_mailboxes = set()\n        old_mailboxes = set()\n        for row in db.execute(\"SELECT * FROM `mailboxes` WHERE `app_id`=?\",\n                              (self._app_id,)).fetchall():\n            mailbox_id = row[\"id\"]\n            log.msg(f\"  1: age={now - row['updated']}, old={now - old}, {mailbox_id}\")\n            if row[\"updated\"] > old:\n                new_mailboxes.add(mailbox_id)\n            else:\n                old_mailboxes.add(mailbox_id)\n        log.msg(\" 2: mailboxes:\", new_mailboxes, old_mailboxes)\n\n        old_nameplates = set()\n        for row in db.execute(\"SELECT * FROM `nameplates` WHERE `app_id`=?\",\n                              (self._app_id,)).fetchall():\n            npid = row[\"id\"]\n            mailbox_id = row[\"mailbox_id\"]\n            if mailbox_id in old_mailboxes:\n                old_nameplates.add(npid)\n        log.msg(\" 3: old_nameplates dbids\", old_nameplates)\n\n        for npid in old_nameplates:\n            log.msg(\"  deleting nameplate with dbid\", npid)\n            side_rows = db.execute(\"SELECT * FROM `nameplate_sides`\"\n                                   \" WHERE `nameplates_id`=?\",\n                                   (npid,)).fetchall()\n            db.execute(\"DELETE FROM `nameplate_sides` WHERE `nameplates_id`=?\",\n                       (npid,))\n            db.execute(\"DELETE FROM `nameplates` WHERE `id`=?\", (npid,))\n            if self._usage_db:\n                self._summarize_nameplate_and_store(side_rows, now, pruned=True)\n            modified = True\n\n        # delete all messages for old mailboxes\n        # delete all old mailboxes\n\n        for mailbox_id in old_mailboxes:\n            log.msg(\"  deleting mailbox\", mailbox_id)\n            row = db.execute(\"SELECT * FROM `mailboxes`\"\n                             \" WHERE `id`=?\", (mailbox_id,)).fetchone()\n            for_nameplate = row[\"for_nameplate\"]\n            side_rows = db.execute(\"SELECT * FROM `mailbox_sides`\"\n                                   \" WHERE `mailbox_id`=?\",\n                                   (mailbox_id,)).fetchall()\n            db.execute(\"DELETE FROM `messages` WHERE `mailbox_id`=?\",\n                       (mailbox_id,))\n            db.execute(\"DELETE FROM `mailbox_sides` WHERE `mailbox_id`=?\",\n                       (mailbox_id,))\n            db.execute(\"DELETE FROM `mailboxes` WHERE `id`=?\",\n                       (mailbox_id,))\n            if self._usage_db:\n                self._summarize_mailbox_and_store(for_nameplate, side_rows,\n                                                  now, pruned=True)\n            modified = True\n\n        if modified:\n            db.commit()\n            if self._usage_db:\n                self._usage_db.commit()\n        in_use = bool(self._mailboxes)\n        log.msg(f\"  prune complete, modified={modified}, in_use={in_use}\")\n        return in_use\n\n    def count_listeners(self):\n        return sum(mailbox.count_listeners()\n                   for mailbox in self._mailboxes.values())\n\n    def _shutdown(self):\n        for channel in self._mailboxes.values():\n            channel._shutdown()\n\n\nclass Server(service.MultiService):\n    def __init__(self, db, allow_list, welcome,\n                 blur_usage, usage_db=None, log_file=None):\n        service.MultiService.__init__(self)\n        self._db = db\n        self._allow_list = allow_list\n        self._welcome = welcome\n        self._blur_usage = blur_usage\n        self._log_requests = blur_usage is None\n        self._usage_db = usage_db\n        self._log_file = log_file\n        self._apps = {}\n\n    def get_welcome(self):\n        return self._welcome\n    def get_log_requests(self):\n        return self._log_requests\n\n    def get_app(self, app_id):\n        assert isinstance(app_id, str)\n        if not app_id in self._apps:\n            if self._log_requests:\n                log.msg(f\"spawning app_id {app_id}\")\n            self._apps[app_id] = AppNamespace(\n                self._db,\n                self._usage_db,\n                self._blur_usage,\n                self._log_requests,\n                app_id,\n                self._allow_list,\n            )\n        return self._apps[app_id]\n\n    def get_all_apps(self):\n        apps = set()\n        for row in self._db.execute(\"SELECT DISTINCT `app_id`\"\n                                    \" FROM `nameplates`\").fetchall():\n            apps.add(row[\"app_id\"])\n        for row in self._db.execute(\"SELECT DISTINCT `app_id`\"\n                                    \" FROM `mailboxes`\").fetchall():\n            apps.add(row[\"app_id\"])\n        for row in self._db.execute(\"SELECT DISTINCT `app_id`\"\n                                    \" FROM `messages`\").fetchall():\n            apps.add(row[\"app_id\"])\n        return apps\n\n    def prune_all_apps(self, now, old):\n        # As with AppNamespace.prune_old_mailboxes, we log for now.\n        log.msg(\"beginning app prune\")\n        for app_id in sorted(self.get_all_apps()):\n            log.msg(f\" app prune checking {app_id!r}\")\n            app = self.get_app(app_id)\n            in_use = app.prune(now, old)\n            if not in_use:\n                del self._apps[app_id]\n        log.msg(f\"app prune ends, {len(self._apps)} apps\")\n\n    def dump_stats(self, now, rebooted):\n        if not self._usage_db:\n            return\n        # write everything to self._usage_db\n\n        # Most of our current-status state is recorded in the channel_db, and\n        # our historical state goes into the usage_db. Both are updated each\n        # time something changes, so stats monitors can just read things out\n        # from there. The one bit of runtime state that isn't recorded each\n        # time is the number of connected clients, which will differ from the\n        # number of live \"sides\" briefly after they disconnect but before the\n        # mailbox is closed.\n\n        connections = sum(app.count_listeners()\n                          for app in self._apps.values())\n        # TODO: this is all connections, not just the websocket ones. We don't\n        # have non-websocket connections yet, but when we add them, this needs\n        # to be updated. Probably by asking the WebSocketServerFactory to count\n        # them.\n        self._usage_db.execute(\"DELETE FROM `current`\")\n        self._usage_db.execute(\"INSERT INTO `current`\"\n                               \" (`rebooted`, `updated`, `blur_time`,\"\n                               \"  `connections_websocket`)\"\n                               \" VALUES(?,?,?,?)\",\n                               (rebooted, now, self._blur_usage, connections))\n        self._usage_db.commit()\n\n        # current status: expected to be zero most of the time\n        #c[\"nameplates_total\"] = q(\"SELECT COUNT() FROM `nameplates`\")\n        # TODO: nameplates with only one side (most of them)\n        # TODO: nameplates with two sides (very fleeting)\n        # TODO: nameplates with three or more sides (crowded, unlikely)\n        #c[\"mailboxes_total\"] = q(\"SELECT COUNT() FROM `mailboxes`\")\n        # TODO: mailboxes with only one side (most of them)\n        # TODO: mailboxes with two sides (somewhat fleeting, in-transit)\n        # TODO: mailboxes with three or more sides (unlikely)\n        #c[\"messages_total\"] = q(\"SELECT COUNT() FROM `messages`\")\n\n        # recent timings (last 100 operations)\n        # TODO: median/etc of nameplate.total_time\n        # TODO: median/etc of mailbox.waiting_time (should be the same)\n        # TODO: median/etc of mailbox.total_time\n\n        # other\n        # TODO: mailboxes without nameplates (needs new DB schema)\n\n    def startService(self):\n        service.MultiService.startService(self)\n        log.msg(\"Wormhole relay server running\")\n        if self._blur_usage:\n            log.msg(\"blurring access times to %d seconds\" % self._blur_usage)\n            #log.msg(\"not logging HTTP requests\")\n        else:\n            log.msg(\"not blurring access times\")\n        if not self._allow_list:\n            log.msg(\"listing of allocated nameplates disallowed\")\n\n    def stopService(self):\n        # This forcibly boots any clients that are still connected, which\n        # helps with unit tests that use threads for both clients. One client\n        # hits an exception, which terminates the test (and .tearDown calls\n        # stopService on the relay), but the other client (in its thread) is\n        # still waiting for a message. By killing off all connections, that\n        # other client gets an error, and exits promptly.\n        for app in self._apps.values():\n            app._shutdown()\n        return service.MultiService.stopService(self)\n\ndef make_server(db, allow_list=True,\n                advertise_version=None,\n                signal_error=None,\n                blur_usage=None,\n                usage_db=None,\n                log_file=None,\n                welcome_motd=None,\n                ):\n    if blur_usage:\n        log.msg(\"blurring access times to %d seconds\" % blur_usage)\n    else:\n        log.msg(\"not blurring access times\")\n\n    welcome = dict()\n    if welcome_motd is not None:\n        # adding .motd will cause all clients to display the message,\n        # then keep running normally\n        welcome[\"motd\"] = str(welcome_motd)\n\n    if advertise_version:\n        # The primary (python CLI) implementation will emit a message if\n        # its version does not match this key. If/when we have\n        # distributions which include older version, but we still expect\n        # them to be compatible, stop sending this key.\n        welcome[\"current_cli_version\"] = advertise_version\n\n    if signal_error:\n        welcome[\"error\"] = signal_error\n\n    return Server(db, allow_list=allow_list, welcome=welcome,\n                  blur_usage=blur_usage, usage_db=usage_db, log_file=log_file)\n"
  },
  {
    "path": "src/wormhole_mailbox_server/server_tap.py",
    "content": "import os, json, time\nfrom twisted.internet import reactor\nfrom twisted.python import usage, log\nfrom twisted.application.service import MultiService\nfrom twisted.application.internet import (TimerService,\n                                          StreamServerEndpointService)\nfrom twisted.internet import endpoints\nfrom .increase_rlimits import increase_rlimits\nfrom .server import make_server\nfrom .web import make_web_server\nfrom .database import create_or_upgrade_channel_db, create_or_upgrade_usage_db\n\nLONGDESC = \"\"\"This plugin sets up a 'Mailbox' server for magic-wormhole.\nThis service forwards short messages between clients, to perform key exchange\nand connection setup.\"\"\"\n\nclass Options(usage.Options):\n    synopsis = \"[--port=] [--log-fd] [--blur-usage=] [--usage-db=]\"\n    longdesc = LONGDESC\n\n    optParameters = [\n        (\"port\", \"p\", r\"tcp:4000:interface=\\:\\:\", \"endpoint to listen on\"),\n        (\"blur-usage\", None, None, \"round logged access times to improve privacy\"),\n        (\"log-fd\", None, None, \"write JSON usage logs to this file descriptor\"),\n        (\"channel-db\", None, \"relay.sqlite\", \"location for the state database\"),\n        (\"usage-db\", None, None, \"record usage data (SQLite)\"),\n        (\"advertise-version\", None, None, \"version to recommend to clients\"),\n        (\"signal-error\", None, None, \"force all clients to fail with a message\"),\n        (\"motd\", None, None, \"Send a Message of the Day in the welcome\"),\n        ]\n    optFlags = [\n        (\"disallow-list\", None, \"refuse to send list of allocated nameplates\"),\n        ]\n\n    def __init__(self):\n        super().__init__()\n        self[\"websocket-protocol-options\"] = []\n        self[\"allow-list\"] = True\n\n    def opt_disallow_list(self):\n        self[\"allow-list\"] = False\n\n    def opt_log_fd(self, arg):\n        self[\"log-fd\"] = int(arg)\n\n    def opt_blur_usage(self, arg):\n        # --blur-usage= is in seconds. If the option isn't provided, we'll keep\n        # the default of None\n        self[\"blur-usage\"] = int(arg)\n\n    def opt_websocket_protocol_option(self, arg):\n        \"\"\"A websocket server protocol option to configure: OPTION=VALUE. This option can be provided multiple times.\"\"\"\n        try:\n            key, value = arg.split(\"=\", 1)\n        except ValueError:\n            raise usage.UsageError(\"format options as OPTION=VALUE\")\n        try:\n            value = json.loads(value)\n        except:\n            raise usage.UsageError(f\"could not parse JSON value for {key}\")\n        self[\"websocket-protocol-options\"].append((key, value))\n\n\nSECONDS = 1.0\nMINUTE = 60*SECONDS\n\n# CHANNEL_EXPIRATION_TIME should be longer than EXPIRATION_CHECK_PERIOD\nCHANNEL_EXPIRATION_TIME = 11*MINUTE\nEXPIRATION_CHECK_PERIOD = 5*MINUTE\n\ndef makeService(config, channel_db=\"relay.sqlite\", reactor=reactor):\n    increase_rlimits()\n\n    parent = MultiService()\n\n    channel_db = create_or_upgrade_channel_db(config[\"channel-db\"])\n    usage_db = create_or_upgrade_usage_db(config[\"usage-db\"])\n    log_file = (os.fdopen(int(config[\"log-fd\"]), \"w\")\n                if config[\"log-fd\"] is not None\n                else None)\n    server = make_server(channel_db,\n                         allow_list=config[\"allow-list\"],\n                         advertise_version=config[\"advertise-version\"],\n                         signal_error=config[\"signal-error\"],\n                         blur_usage=config[\"blur-usage\"],\n                         usage_db=usage_db,\n                         log_file=log_file,\n                         welcome_motd=config[\"motd\"],\n                         )\n    server.setServiceParent(parent)\n    rebooted = time.time()\n    def expire():\n        now = time.time()\n        old = now - CHANNEL_EXPIRATION_TIME\n        try:\n            server.prune_all_apps(now, old)\n        except Exception as e:\n            # catch-and-log exceptions during prune, so a single error won't\n            # kill the loop. See #13 for details.\n            log.msg(\"error during prune_all_apps\")\n            log.err(e)\n        server.dump_stats(now, rebooted=rebooted)\n    TimerService(EXPIRATION_CHECK_PERIOD, expire).setServiceParent(parent)\n\n    log_requests = config[\"blur-usage\"] is None\n    site = make_web_server(server, log_requests,\n                           config[\"websocket-protocol-options\"])\n    ep = endpoints.serverFromString(reactor, config[\"port\"]) # to listen\n    StreamServerEndpointService(ep, site).setServiceParent(parent)\n    log.msg(\"websocket listening on ws://HOSTNAME:PORT/v1\")\n\n    return parent\n"
  },
  {
    "path": "src/wormhole_mailbox_server/server_websocket.py",
    "content": "import time\nfrom twisted.internet import reactor\nfrom twisted.python import log\nfrom autobahn.twisted import websocket\nfrom .server import CrowdedError, ReclaimedError, SidedMessage, check_valid_nameplate\nfrom .util import dict_to_bytes, bytes_to_dict\n\n# The WebSocket allows the client to send \"commands\" to the server, and the\n# server to send \"responses\" to the client. Note that commands and responses\n# are not necessarily one-to-one. All commands provoke an \"ack\" response\n# (with a copy of the original message) for timing, testing, and\n# synchronization purposes. All commands and responses are JSON-encoded.\n\n# Each WebSocket connection is bound to one \"appid\" and one \"side\", which are\n# set by the \"bind\" command (which must be the first command on the\n# connection), and must be set before any other command will be accepted.\n\n# Each connection can be bound to a single \"mailbox\" (a two-sided\n# store-and-forward queue, identified by the \"mailbox id\": a long, randomly\n# unique string identifier) by using the \"open\" command. This protects the\n# mailbox from idle closure, enables the \"add\" command (to put new messages\n# in the queue), and triggers delivery of past and future messages via the\n# \"message\" response. The \"close\" command removes the binding (but note that\n# it does not enable the subsequent binding of a second mailbox). When the\n# last side closes a mailbox, its contents are deleted.\n\n# Additionally, the connection can be bound a single \"nameplate\", which is\n# short identifier that makes up the first component of a wormhole code. Each\n# nameplate points to a single long-id \"mailbox\". The \"allocate\" message\n# determines the shortest available numeric nameplate, reserves it, and\n# returns the nameplate id. \"list\" returns a list of all numeric nameplates\n# which currently have only one side active (i.e. they are waiting for a\n# partner). The \"claim\" message reserves an arbitrary nameplate id (perhaps\n# the receiver of a wormhole connection typed in a code they got from the\n# sender, or perhaps the two sides agreed upon a code offline and are both\n# typing it in), and the \"release\" message releases it. When every side that\n# has claimed the nameplate has also released it, the nameplate is\n# deallocated (but they will probably keep the underlying mailbox open).\n\n# \"claim\" and \"release\" may only be called once per connection, however calls\n# across connections (assuming a consistent \"side\") are idempotent. [connect,\n# claim, disconnect, connect, claim] is legal, but not useful, as is a\n# \"release\" for a nameplate that nobody is currently claiming.\n\n# \"open\" and \"close\" may only be called once per connection. They are\n# basically idempotent, however \"open\" doubles as a subscribe action. So\n# [connect, open, disconnect, connect, open] is legal *and* useful (without\n# the second \"open\", the second connection would not be subscribed to hear\n# about new messages).\n\n# Inbound (client to server) commands are marked as \"->\" below. Unrecognized\n# inbound keys will be ignored. Outbound (server to client) responses use\n# \"<-\". There is no guaranteed correlation between requests and responses. In\n# this list, \"A -> B\" means that some time after A is received, at least one\n# message of type B will be sent out (probably).\n\n# All responses include a \"server_tx\" key, which is a float (seconds since\n# epoch) with the server clock just before the outbound response was written\n# to the socket.\n\n# connection -> welcome\n#  <- {type: \"welcome\", welcome: {}} # .welcome keys are all optional:\n#        current_cli_version: out-of-date clients display a warning\n#        motd: all clients display message, then continue normally\n#        error: all clients display mesage, then terminate with error\n# -> {type: \"bind\", appid:, side:}\n#\n# -> {type: \"list\"} -> nameplates\n#  <- {type: \"nameplates\", nameplates: [{id: str,..},..]}\n# -> {type: \"allocate\"} -> nameplate, mailbox\n#  <- {type: \"allocated\", nameplate: str}\n# -> {type: \"claim\", nameplate: str} -> mailbox\n#  <- {type: \"claimed\", mailbox: str}\n# -> {type: \"release\"}\n#     .nameplate is optional, but must match previous claim()\n#  <- {type: \"released\"}\n#\n# -> {type: \"open\", mailbox: str} -> message\n#     sends old messages now, and subscribes to deliver future messages\n#  <- {type: \"message\", side:, phase:, body:, msg_id:}} # body is hex\n# -> {type: \"add\", phase: str, body: hex} # will send echo in a \"message\"\n#\n# -> {type: \"close\", mood: str} -> closed\n#     .mailbox is optional, but must match previous open()\n#  <- {type: \"closed\"}\n#\n#  <- {type: \"error\", error: str, orig: {}} # in response to malformed msgs\n\n# for tests that need to know when a message has been processed:\n# -> {type: \"ping\", ping: int} -> pong (does not require bind/claim)\n#  <- {type: \"pong\", pong: int}\n\nclass Error(Exception):\n    def __init__(self, explain):\n        self._explain = explain\n\nclass WebSocketServer(websocket.WebSocketServerProtocol):\n    def __init__(self):\n        websocket.WebSocketServerProtocol.__init__(self)\n        self._app = None\n        self._side = None\n        self._did_allocate = False # only one allocate() per websocket\n        self._listening = False\n        self._did_claim = False\n        self._nameplate_id = None\n        self._did_release = False\n        self._did_open = False\n        self._mailbox = None\n        self._mailbox_id = None\n        self._did_close = False\n        self._peer_addr_port = None\n\n    def onConnect(self, request):\n        rv = self.factory._server\n        # Caddy uses capitalized headers like X-Real-IP and X-Real-Port, which\n        # you see if you forward Caddy to netcat. But the twisted/autobahn\n        # Request object lowercases everything.\n        #\n        # We only use this for assistance in NAT hole-punching, so it\n        # doesn't matter if the client is able to inject their own\n        # headers and spoof somebody else's IP address, they're only\n        # hurting themselves\n        if \"x-real-ip\" in request.headers:\n            peer_host = request.headers.get(\"x-real-ip\") # either 1.2.3.4 or 2600:..:1234\n            peer_port = request.headers.get(\"x-real-port\")\n            # assume frontends like Caddy don't give us v4-in-v6 addrs\n            peer_type = \"ipv6\" if \":\" in peer_host else \"ipv4\"\n        else:\n            peer = request.peer\n            peer_type = peer.split(\":\", maxsplit=1)[0]\n            peer_port = peer.rsplit(\":\", maxsplit=1)[-1]\n            peer_host = peer[len(peer_type)+1:(-len(peer_port)-1)]\n            # this gets me [tcp6, ::1, 53276] for a client using ws://localhost:4000/v1\n            #  or ws://[::1]:4000/v1\n            # and [tcp6, ::ffff:127.0.0.1, 53279] when using ws://127.0.0.1:4000/v1\n            if peer_type == \"tcp6\":\n                peer_type = \"ipv6\"\n                if peer_host.startswith(\"::ffff:\"):\n                    peer_host = peer_host.rsplit(\":\", maxsplit=1)[-1]\n                    peer_type = \"ipv4\"\n            else:\n                peer_type = \"ipv4\"\n        self._peer_addr_port = (peer_type, peer_host, int(peer_port))\n\n        if rv.get_log_requests():\n            v = 4 if peer_type == \"ipv4\" else 6\n            log.msg(f\"ws client connecting: tcp{v}:{peer_host}:{peer_port}\")\n        self._reactor = self.factory.reactor\n        # can return (name, dict) or name here, where name is\n        # WebSocket subprotocol name and dict is extra headers (if\n        # provided) to send\n\n    def get_your_address(self):\n        (peer_type, peer_host, peer_port) = self._peer_addr_port\n        you = { \"port\": peer_port }\n        if peer_type == \"ipv4\":\n            you[\"ipv4\"] = peer_host\n        elif peer_type == \"ipv6\":\n            you[\"ipv6\"] = peer_host\n        return you\n\n    def onOpen(self):\n        rv = self.factory._server\n        welcome = rv.get_welcome().copy()\n        welcome[\"your-address\"] = self.get_your_address()\n        self.send(\"welcome\", welcome=welcome)\n\n    def onMessage(self, payload, isBinary):\n        server_rx = time.time()\n        msg = bytes_to_dict(payload)\n        try:\n            if \"type\" not in msg:\n                raise Error(\"missing 'type'\")\n            self.send(\"ack\", id=msg.get(\"id\"))\n\n            mtype = msg[\"type\"]\n            if mtype == \"ping\":\n                return self.handle_ping(msg)\n            if mtype == \"bind\":\n                return self.handle_bind(msg, server_rx)\n\n            if not self._app:\n                raise Error(\"must bind first\")\n            if mtype == \"list\":\n                return self.handle_list()\n            if mtype == \"allocate\":\n                return self.handle_allocate(server_rx)\n            if mtype == \"claim\":\n                return self.handle_claim(msg, server_rx)\n            if mtype == \"release\":\n                return self.handle_release(msg, server_rx)\n\n            if mtype == \"open\":\n                return self.handle_open(msg, server_rx)\n            if mtype == \"add\":\n                return self.handle_add(msg, server_rx)\n            if mtype == \"close\":\n                return self.handle_close(msg, server_rx)\n\n            raise Error(\"unknown type\")\n        except Error as e:\n            self.send(\"error\", error=e._explain, orig=msg)\n\n    def handle_ping(self, msg):\n        if \"ping\" not in msg:\n            raise Error(\"ping requires 'ping'\")\n        self.send(\"pong\", pong=msg[\"ping\"])\n\n    def handle_bind(self, msg, server_rx):\n        if self._app or self._side:\n            raise Error(\"already bound\")\n        if \"appid\" not in msg:\n            raise Error(\"bind requires 'appid'\")\n        if \"side\" not in msg:\n            raise Error(\"bind requires 'side'\")\n        self._app = self.factory._server.get_app(msg[\"appid\"])\n        self._side = msg[\"side\"]\n        client_version = msg.get(\"client_version\", (None, None))\n        # e.g. (\"python\", \"0.xyz\") . <=0.10.5 did not send client_version\n        self._app.log_client_version(server_rx, self._side, client_version)\n\n\n    def handle_list(self):\n        nameplate_ids = sorted(self._app.get_nameplate_ids())\n        # provide room to add nameplate attributes later (like which wordlist\n        # is used for each, maybe how many words)\n        nameplates = [{\"id\": nid} for nid in nameplate_ids]\n        self.send(\"nameplates\", nameplates=nameplates)\n\n    def handle_allocate(self, server_rx):\n        if self._did_allocate:\n            raise Error(\"you already allocated one, don't be greedy\")\n        nameplate_id = self._app.allocate_nameplate(self._side, server_rx)\n        assert isinstance(nameplate_id, str)\n        self._did_allocate = True\n        self.send(\"allocated\", nameplate=nameplate_id)\n\n    def handle_claim(self, msg, server_rx):\n        if \"nameplate\" not in msg:\n            raise Error(\"claim requires 'nameplate'\")\n        if self._did_claim:\n            raise Error(\"only one claim per connection\")\n        self._did_claim = True\n        nameplate_id = msg[\"nameplate\"]\n        check_valid_nameplate(nameplate_id)\n        self._nameplate_id = nameplate_id\n        try:\n            mailbox_id = self._app.claim_nameplate(nameplate_id, self._side,\n                                                   server_rx)\n        except CrowdedError:\n            raise Error(\"crowded\")\n        except ReclaimedError:\n            raise Error(\"reclaimed\")\n        self.send(\"claimed\", mailbox=mailbox_id)\n\n    def handle_release(self, msg, server_rx):\n        if self._did_release:\n            raise Error(\"only one release per connection\")\n        if \"nameplate\" in msg:\n            if self._nameplate_id is not None:\n                # we only care about equality, don't bother with\n                # check_valid_nameplate()\n                if msg[\"nameplate\"] != self._nameplate_id:\n                    raise Error(\"release and claim must use same nameplate\")\n            nameplate_id = msg[\"nameplate\"]\n        else:\n            if self._nameplate_id is None:\n                raise Error(\"release without nameplate must follow claim\")\n            nameplate_id = self._nameplate_id\n        assert nameplate_id is not None\n        self._did_release = True\n        self._app.release_nameplate(nameplate_id, self._side, server_rx)\n        self.send(\"released\")\n\n\n    def handle_open(self, msg, server_rx):\n        if self._mailbox:\n            raise Error(\"only one open per connection\")\n        if \"mailbox\" not in msg:\n            raise Error(\"open requires 'mailbox'\")\n        mailbox_id = msg[\"mailbox\"]\n        assert isinstance(mailbox_id, str)\n        self._mailbox_id = mailbox_id\n        try:\n            self._mailbox = self._app.open_mailbox(mailbox_id, self._side,\n                                                   server_rx)\n        except CrowdedError:\n            raise Error(\"crowded\")\n        def _send(sm):\n            self.send(\"message\", side=sm.side, phase=sm.phase,\n                      body=sm.body, server_rx=sm.server_rx, id=sm.msg_id)\n        def _stop():\n            pass\n        self._listening = True\n        for old_sm in self._mailbox.add_listener(self, _send, _stop):\n            _send(old_sm)\n\n    def handle_add(self, msg, server_rx):\n        if not self._mailbox:\n            raise Error(\"must open mailbox before adding\")\n        if \"phase\" not in msg:\n            raise Error(\"missing 'phase'\")\n        if \"body\" not in msg:\n            raise Error(\"missing 'body'\")\n        msg_id = msg.get(\"id\") # optional\n        sm = SidedMessage(side=self._side, phase=msg[\"phase\"],\n                          body=msg[\"body\"], server_rx=server_rx,\n                          msg_id=msg_id)\n        self._mailbox.add_message(sm)\n\n    def handle_close(self, msg, server_rx):\n        if self._did_close:\n            raise Error(\"only one close per connection\")\n        if \"mailbox\" in msg:\n            if self._mailbox_id is not None:\n                if msg[\"mailbox\"] != self._mailbox_id:\n                    raise Error(\"open and close must use same mailbox\")\n            mailbox_id = msg[\"mailbox\"]\n        else:\n            if self._mailbox_id is None:\n                raise Error(\"close without mailbox must follow open\")\n            mailbox_id = self._mailbox_id\n        if not self._mailbox:\n            try:\n                self._mailbox = self._app.open_mailbox(mailbox_id, self._side,\n                                                       server_rx)\n            except CrowdedError:\n                raise Error(\"crowded\")\n        if self._listening:\n            self._mailbox.remove_listener(self)\n            self._listening = False\n        self._did_close = True\n        self._mailbox.close(self._side, msg.get(\"mood\"), server_rx)\n        self._mailbox = None\n        self.send(\"closed\")\n\n    def send(self, mtype, **kwargs):\n        kwargs[\"type\"] = mtype\n        kwargs[\"server_tx\"] = time.time()\n        payload = dict_to_bytes(kwargs)\n        self.sendMessage(payload, False)\n\n    def onClose(self, wasClean, code, reason):\n        #log.msg(\"onClose\", self, self._mailbox, self._listening)\n        if self._mailbox and self._listening:\n            self._mailbox.remove_listener(self)\n\n\nclass WebSocketServerFactory(websocket.WebSocketServerFactory):\n    protocol = WebSocketServer\n\n    def __init__(self, url, server):\n        websocket.WebSocketServerFactory.__init__(self, url)\n        self.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600)\n        # note: Autobahn uses \"self.factory.server\" for the Server\n        # version string, so we musn't use that as well.\n        self._server = server\n        from . import __version__\n        self.server = f\"Magic Wormhole Mailbox {__version__}\"\n        self.reactor = reactor # for tests to control\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/__init__.py",
    "content": ""
  },
  {
    "path": "src/wormhole_mailbox_server/test/common.py",
    "content": "#from __future__ import unicode_literals\nfrom twisted.internet import reactor, endpoints\nfrom twisted.internet.defer import inlineCallbacks\nfrom ..database import create_or_upgrade_channel_db, create_or_upgrade_usage_db\nfrom ..server import make_server\nfrom ..web import make_web_server\n\nclass ServerBase:\n    log_requests = False\n\n    @inlineCallbacks\n    def setUp(self):\n        self._lp = None\n        if self.log_requests:\n            blur_usage = None\n        else:\n            blur_usage = 60.0\n        usage_db = create_or_upgrade_usage_db(\":memory:\")\n        yield self._setup_relay(blur_usage=blur_usage, usage_db=usage_db)\n\n    @inlineCallbacks\n    def _setup_relay(self, do_listen=False, web_log_requests=False, **kwargs):\n        channel_db = create_or_upgrade_channel_db(\":memory:\")\n        self._server = make_server(channel_db, **kwargs)\n        if do_listen:\n            ep = endpoints.TCP4ServerEndpoint(reactor, 0, interface=\"127.0.0.1\")\n            self._site = make_web_server(self._server,\n                                         log_requests=web_log_requests)\n            self._lp = yield ep.listen(self._site)\n            addr = self._lp.getHost()\n            self.relayurl = \"ws://127.0.0.1:%d/v1\" % addr.port\n            self.rdv_ws_port = addr.port\n\n    def tearDown(self):\n        if self._lp:\n            return self._lp.stopListening()\n\nclass _Util:\n    def _nameplate(self, app, name):\n        np_row = app._db.execute(\"SELECT * FROM `nameplates`\"\n                                 \" WHERE `app_id`='appid' AND `name`=?\",\n                                 (name,)).fetchone()\n        if not np_row:\n            return None, None\n        npid = np_row[\"id\"]\n        side_rows = app._db.execute(\"SELECT * FROM `nameplate_sides`\"\n                                    \" WHERE `nameplates_id`=?\",\n                                    (npid,)).fetchall()\n        return np_row, side_rows\n\n    def _mailbox(self, app, mailbox_id):\n        mb_row = app._db.execute(\"SELECT * FROM `mailboxes`\"\n                                 \" WHERE `app_id`='appid' AND `id`=?\",\n                                 (mailbox_id,)).fetchone()\n        if not mb_row:\n            return None, None\n        side_rows = app._db.execute(\"SELECT * FROM `mailbox_sides`\"\n                                    \" WHERE `mailbox_id`=?\",\n                                    (mailbox_id,)).fetchall()\n        return mb_row, side_rows\n\n    def _messages(self, app):\n        c = app._db.execute(\"SELECT * FROM `messages`\"\n                            \" WHERE `app_id`='appid' AND `mailbox_id`='mid'\")\n        return c.fetchall()\n\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_config.py",
    "content": "from twisted.python.usage import UsageError\nfrom twisted.trial import unittest\nfrom .. import server_tap\n\nPORT = r\"tcp:4000:interface=\\:\\:\"\n\nclass Config(unittest.TestCase):\n    def test_defaults(self):\n        o = server_tap.Options()\n        o.parseOptions([])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_advertise_version(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--advertise-version=1.0\"])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": \"1.0\",\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_blur(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--blur-usage=60\"])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": 60,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_channel_db(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--channel-db=other.sqlite\"])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"other.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_disallow_list(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--disallow-list\"])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": False,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_log_fd(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--log-fd=5\"])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": 5,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_port(self):\n        o = server_tap.Options()\n        o.parseOptions([\"-p\", \"tcp:5555\"])\n        self.assertEqual(o, {\"port\": \"tcp:5555\",\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n        o = server_tap.Options()\n        o.parseOptions([\"--port=tcp:5555\"])\n        self.assertEqual(o, {\"port\": \"tcp:5555\",\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_signal_error(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--signal-error=ohnoes\"])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": \"ohnoes\",\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_usage_db(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--usage-db=usage.sqlite\"])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": \"usage.sqlite\",\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [],\n                             })\n\n    def test_websocket_protocol_option_1(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--websocket-protocol-option\", 'foo=\"bar\"'])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [(\"foo\", \"bar\")],\n                             })\n\n    def test_websocket_protocol_option_2(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--websocket-protocol-option\", 'foo=\"bar\"',\n                        \"--websocket-protocol-option\", 'baz=[1,\"buz\"]',\n                        ])\n        self.assertEqual(o, {\"port\": PORT,\n                             \"channel-db\": \"relay.sqlite\",\n                             \"disallow-list\": 0,\n                             \"allow-list\": True,\n                             \"advertise-version\": None,\n                             \"signal-error\": None,\n                             \"usage-db\": None,\n                             \"blur-usage\": None,\n                             \"motd\": None,\n                             \"log-fd\": None,\n                             \"websocket-protocol-options\": [(\"foo\", \"bar\"),\n                                                            (\"baz\", [1, \"buz\"]),\n                                                            ],\n                             })\n\n    def test_websocket_protocol_option_errors(self):\n        o = server_tap.Options()\n        with self.assertRaises(UsageError):\n            o.parseOptions([\"--websocket-protocol-option\", 'foo'])\n        with self.assertRaises(UsageError):\n            # I would be nice if this worked, but the 'bar' isn't JSON. To\n            # enable passing lists and more complicated things as values,\n            # simple string values must be passed with additional quotes\n            # (e.g. '\"bar\"')\n            o.parseOptions([\"--websocket-protocol-option\", 'foo=bar'])\n\n    def test_string(self):\n        o = server_tap.Options()\n        s = str(o)\n        self.assertIn(\"This plugin sets up a 'Mailbox' server\", s)\n        self.assertIn(\"--blur-usage=\", s)\n        self.assertIn(\"round logged access times to improve privacy\", s)\n\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_database.py",
    "content": "import os\nfrom twisted.python import filepath\nfrom twisted.trial import unittest\nfrom .. import database\nfrom ..database import (CHANNELDB_TARGET_VERSION, USAGEDB_TARGET_VERSION,\n                        _get_db, dump_db, DBError)\n\nclass Get(unittest.TestCase):\n    def test_create_default(self):\n        db_url = \":memory:\"\n        db = _get_db(db_url, \"channel\", CHANNELDB_TARGET_VERSION)\n        rows = db.execute(\"SELECT * FROM version\").fetchall()\n        self.assertEqual(len(rows), 1)\n        self.assertEqual(rows[0][\"version\"], CHANNELDB_TARGET_VERSION)\n\n    def test_open_existing_file(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"normal.db\")\n        db = _get_db(fn, \"channel\", CHANNELDB_TARGET_VERSION)\n        rows = db.execute(\"SELECT * FROM version\").fetchall()\n        self.assertEqual(len(rows), 1)\n        self.assertEqual(rows[0][\"version\"], CHANNELDB_TARGET_VERSION)\n        db2 = _get_db(fn, \"channel\", CHANNELDB_TARGET_VERSION)\n        rows = db2.execute(\"SELECT * FROM version\").fetchall()\n        self.assertEqual(len(rows), 1)\n        self.assertEqual(rows[0][\"version\"], CHANNELDB_TARGET_VERSION)\n\n    def test_open_bad_version(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"old.db\")\n        db = _get_db(fn, \"channel\", CHANNELDB_TARGET_VERSION)\n        db.execute(\"UPDATE version SET version=999\")\n        db.commit()\n\n        with self.assertRaises(DBError) as e:\n            _get_db(fn, \"channel\", CHANNELDB_TARGET_VERSION)\n        self.assertIn(\"Unable to handle db version 999\", str(e.exception))\n\n    def test_open_corrupt(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"corrupt.db\")\n        with open(fn, \"wb\") as f:\n            f.write(b\"I am not a database\")\n        with self.assertRaises(DBError) as e:\n            _get_db(fn, \"channel\", CHANNELDB_TARGET_VERSION)\n        self.assertIn(\"not a database\", str(e.exception))\n\n    def test_failed_create_allows_subsequent_create(self):\n        patch = self.patch(database, \"get_schema\", lambda version: b\"this is a broken schema\")\n        dbfile = filepath.FilePath(self.mktemp())\n        self.assertRaises(Exception, lambda: _get_db(dbfile.path))\n        patch.restore()\n        _get_db(dbfile.path, \"channel\", CHANNELDB_TARGET_VERSION)\n\n    def test_upgrade(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"upgrade.db\")\n        self.assertNotEqual(USAGEDB_TARGET_VERSION, 1)\n\n        # create an old-version DB in a file\n        db = _get_db(fn, \"usage\", 1)\n        rows = db.execute(\"SELECT * FROM version\").fetchall()\n        self.assertEqual(len(rows), 1)\n        self.assertEqual(rows[0][\"version\"], 1)\n        del db\n\n        # then upgrade the file to the latest version\n        dbA = _get_db(fn, \"usage\", USAGEDB_TARGET_VERSION)\n        rows = dbA.execute(\"SELECT * FROM version\").fetchall()\n        self.assertEqual(len(rows), 1)\n        self.assertEqual(rows[0][\"version\"], USAGEDB_TARGET_VERSION)\n        dbA_text = dump_db(dbA)\n        del dbA\n\n        # make sure the upgrades got committed to disk\n        dbB = _get_db(fn, \"usage\", USAGEDB_TARGET_VERSION)\n        dbB_text = dump_db(dbB)\n        del dbB\n        self.assertEqual(dbA_text, dbB_text)\n\n        # The upgraded schema should be equivalent to that of a new DB.\n        latest_db = _get_db(\":memory:\", \"usage\", USAGEDB_TARGET_VERSION)\n        latest_text = dump_db(latest_db)\n        with open(\"up.sql\",\"w\") as f: f.write(dbA_text)\n        with open(\"new.sql\",\"w\") as f: f.write(latest_text)\n        # debug with \"diff -u _trial_temp/up.sql _trial_temp/new.sql\"\n        self.assertEqual(dbA_text, latest_text)\n\n    def test_upgrade_fails(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"upgrade.db\")\n        self.assertNotEqual(USAGEDB_TARGET_VERSION, 1)\n\n        # create an old-version DB in a file\n        db = _get_db(fn, \"usage\", 1)\n        rows = db.execute(\"SELECT * FROM version\").fetchall()\n        self.assertEqual(len(rows), 1)\n        self.assertEqual(rows[0][\"version\"], 1)\n        del db\n\n        # then upgrade the file to a too-new version, for which we have no\n        # upgrader\n        with self.assertRaises(DBError):\n            _get_db(fn, \"usage\", USAGEDB_TARGET_VERSION+1)\n\nclass CreateChannel(unittest.TestCase):\n    def test_memory(self):\n        db = database.create_channel_db(\":memory:\")\n        latest_text = dump_db(db)\n        self.assertIn(\"CREATE TABLE\", latest_text)\n\n    def test_preexisting(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"preexisting.db\")\n        with open(fn, \"w\"):\n            pass\n        with self.assertRaises(database.DBAlreadyExists):\n            database.create_channel_db(fn)\n\n    def test_create(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"created.db\")\n        db = database.create_channel_db(fn)\n        latest_text = dump_db(db)\n        self.assertIn(\"CREATE TABLE\", latest_text)\n\n    def test_create_or_upgrade(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"created.db\")\n        db = database.create_or_upgrade_channel_db(fn)\n        latest_text = dump_db(db)\n        self.assertIn(\"CREATE TABLE\", latest_text)\n\nclass CreateUsage(unittest.TestCase):\n    def test_memory(self):\n        db = database.create_usage_db(\":memory:\")\n        latest_text = dump_db(db)\n        self.assertIn(\"CREATE TABLE\", latest_text)\n\n    def test_preexisting(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"preexisting.db\")\n        with open(fn, \"w\"):\n            pass\n        with self.assertRaises(database.DBAlreadyExists):\n            database.create_usage_db(fn)\n\n    def test_create(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"created.db\")\n        db = database.create_usage_db(fn)\n        latest_text = dump_db(db)\n        self.assertIn(\"CREATE TABLE\", latest_text)\n\n    def test_create_or_upgrade(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"created.db\")\n        db = database.create_or_upgrade_usage_db(fn)\n        latest_text = dump_db(db)\n        self.assertIn(\"CREATE TABLE\", latest_text)\n\n    def test_create_or_upgrade_disabled(self):\n        db = database.create_or_upgrade_usage_db(None)\n        self.assertIs(db, None)\n\nclass OpenChannel(unittest.TestCase):\n    def test_open(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"created.db\")\n        db1 = database.create_channel_db(fn)\n        latest_text = dump_db(db1)\n        self.assertIn(\"CREATE TABLE\", latest_text)\n        db2 = database.open_existing_db(fn)\n        self.assertIn(\"CREATE TABLE\", dump_db(db2))\n\n    def test_doesnt_exist(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"created.db\")\n        with self.assertRaises(database.DBDoesntExist):\n            database.open_existing_db(fn)\n\nclass OpenUsage(unittest.TestCase):\n    def test_open(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"created.db\")\n        db1 = database.create_usage_db(fn)\n        latest_text = dump_db(db1)\n        self.assertIn(\"CREATE TABLE\", latest_text)\n        db2 = database.open_existing_db(fn)\n        self.assertIn(\"CREATE TABLE\", dump_db(db2))\n\n    def test_doesnt_exist(self):\n        basedir = self.mktemp()\n        os.mkdir(basedir)\n        fn = os.path.join(basedir, \"created.db\")\n        with self.assertRaises(database.DBDoesntExist):\n            database.open_existing_db(fn)\n\n\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_rlimits.py",
    "content": "from unittest import mock\nfrom twisted.trial import unittest\nfrom ..increase_rlimits import increase_rlimits\n\nclass RLimits(unittest.TestCase):\n    def test_rlimit(self):\n        def patch_r(name, *args, **kwargs):\n            return mock.patch(\"wormhole_mailbox_server.increase_rlimits.\" + name, *args, **kwargs)\n        fakelog = []\n        def checklog(*expected):\n            self.assertEqual(fakelog, list(expected))\n            fakelog[:] = []\n        NF = \"NOFILE\"\n        mock_NF = patch_r(\"RLIMIT_NOFILE\", NF)\n\n        with patch_r(\"log.msg\", fakelog.append):\n            with patch_r(\"getrlimit\", None):\n                increase_rlimits()\n            checklog(\"unable to import 'resource', leaving rlimit alone\")\n\n            with mock_NF:\n                with patch_r(\"getrlimit\", return_value=(20000, 30000)) as gr:\n                    increase_rlimits()\n                    self.assertEqual(gr.mock_calls, [mock.call(NF)])\n                    checklog(\"RLIMIT_NOFILE.soft was 20000, leaving it alone\")\n\n                with patch_r(\"getrlimit\", return_value=(10, 30000)) as gr:\n                    with patch_r(\"setrlimit\", side_effect=TypeError(\"other\")):\n                        with patch_r(\"log.err\") as err:\n                            increase_rlimits()\n                        self.assertEqual(err.mock_calls, [mock.call()])\n                        checklog(\"changing RLIMIT_NOFILE from (10,30000) to (30000,30000)\",\n                                 \"other error during setrlimit, leaving it alone\")\n\n                    for maxlimit in [40000, 20000, 9000, 2000, 1000]:\n                        def setrlimit(which, newlimit):\n                            if newlimit[0] > maxlimit:\n                                raise ValueError(\"nope\")\n                            return None\n                        calls = []\n                        expected = []\n                        for tries in [30000, 10000, 3200, 1024]:\n                            calls.append(mock.call(NF, (tries, 30000)))\n                            expected.append(\"changing RLIMIT_NOFILE from (10,30000) to (%d,30000)\" % tries)\n                            if tries > maxlimit:\n                                expected.append(\"error during setrlimit: nope\")\n                            else:\n                                expected.append(\"setrlimit successful\")\n                                break\n                        else:\n                            expected.append(\"unable to change rlimit, leaving it alone\")\n\n                        with patch_r(\"setrlimit\", side_effect=setrlimit) as sr:\n                            increase_rlimits()\n                        self.assertEqual(sr.mock_calls, calls)\n                        checklog(*expected)\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_server.py",
    "content": "from unittest import mock\nfrom twisted.trial import unittest\nfrom twisted.python import log\nfrom .common import ServerBase, _Util\nfrom ..server import (make_server, Usage,\n                      SidedMessage, CrowdedError, AppNamespace)\nfrom ..database import create_channel_db, create_usage_db\n\nnpid = \"1\"\n\nclass Server(_Util, ServerBase, unittest.TestCase):\n    def test_apps(self):\n        app1 = self._server.get_app(\"appid1\")\n        self.assertIdentical(app1, self._server.get_app(\"appid1\"))\n        app2 = self._server.get_app(\"appid2\")\n        self.assertNotIdentical(app1, app2)\n\n    def test_nameplate_allocation(self):\n        app = self._server.get_app(\"appid\")\n        nids = set()\n        # this takes a second, and claims all the short-numbered nameplates\n        def add():\n            nameplate_id = app.allocate_nameplate(\"side1\", 0)\n            self.assertEqual(type(nameplate_id), str)\n            nid = int(nameplate_id)\n            nids.add(nid)\n        for i in range(9): add()\n        self.assertNotIn(0, nids)\n        self.assertEqual(set(range(1,10)), nids)\n\n        for i in range(100-10): add()\n        self.assertEqual(len(nids), 99)\n        self.assertEqual(set(range(1,100)), nids)\n\n        for i in range(1000-100): add()\n        self.assertEqual(len(nids), 999)\n        self.assertEqual(set(range(1,1000)), nids)\n\n        add()\n        self.assertEqual(len(nids), 1000)\n        biggest = max(nids)\n        self.assertTrue(1000 <= biggest < 1000000, biggest)\n\n    def test_nameplate_allocation_failure(self):\n        app = self._server.get_app(\"appid\")\n        # pretend to fill all 1M <7-digit nameplates, it should give up\n        # eventually\n        def _get_nameplate_ids():\n            return {\"%d\" % id_int for id_int in range(1, 1000*1000)}\n        app._get_nameplate_ids = _get_nameplate_ids\n        with self.assertRaises(ValueError) as e:\n            app.allocate_nameplate(\"side1\", 0)\n        self.assertIn(\"unable to find a free nameplate-id\", str(e.exception))\n\n    def test_nameplate(self):\n        app = self._server.get_app(\"appid\")\n        name = app.allocate_nameplate(\"side1\", 0)\n        self.assertEqual(type(name), str)\n        nid = int(name)\n        self.assertTrue(0 < nid < 10, nid)\n        self.assertEqual(app.get_nameplate_ids(), {name})\n        # allocate also does a claim\n        np_row, side_rows = self._nameplate(app, name)\n        self.assertEqual(len(side_rows), 1)\n        self.assertEqual(side_rows[0][\"side\"], \"side1\")\n        self.assertEqual(side_rows[0][\"added\"], 0)\n\n        # duplicate claims by the same side are combined\n        mailbox_id = app.claim_nameplate(name, \"side1\", 1)\n        self.assertEqual(type(mailbox_id), str)\n        self.assertEqual(mailbox_id, np_row[\"mailbox_id\"])\n        np_row, side_rows = self._nameplate(app, name)\n        self.assertEqual(len(side_rows), 1)\n        self.assertEqual(side_rows[0][\"added\"], 0)\n        self.assertEqual(mailbox_id, np_row[\"mailbox_id\"])\n\n        # and they don't updated the 'added' time\n        mailbox_id2 = app.claim_nameplate(name, \"side1\", 2)\n        self.assertEqual(mailbox_id, mailbox_id2)\n        np_row, side_rows = self._nameplate(app, name)\n        self.assertEqual(len(side_rows), 1)\n        self.assertEqual(side_rows[0][\"added\"], 0)\n\n        # claim by the second side is new\n        mailbox_id3 = app.claim_nameplate(name, \"side2\", 3)\n        self.assertEqual(mailbox_id, mailbox_id3)\n        np_row, side_rows = self._nameplate(app, name)\n        self.assertEqual(len(side_rows), 2)\n        self.assertEqual(sorted([row[\"side\"] for row in side_rows]),\n                         sorted([\"side1\", \"side2\"]))\n        self.assertIn((\"side2\", 3),\n                      [(row[\"side\"], row[\"added\"]) for row in side_rows])\n\n        # a third claim marks the nameplate as \"crowded\", and adds a third\n        # claim (which must be released later), but leaves the two existing\n        # claims alone\n        self.assertRaises(CrowdedError,\n                          app.claim_nameplate, name, \"side3\", 4)\n        np_row, side_rows = self._nameplate(app, name)\n        self.assertEqual(len(side_rows), 3)\n\n        # releasing a non-existent nameplate is ignored\n        app.release_nameplate(name+\"not\", \"side4\", 0)\n\n        # releasing a side that never claimed the nameplate is ignored\n        app.release_nameplate(name, \"side4\", 0)\n        np_row, side_rows = self._nameplate(app, name)\n        self.assertEqual(len(side_rows), 3)\n\n        # releasing one side leaves the second claim\n        app.release_nameplate(name, \"side1\", 5)\n        np_row, side_rows = self._nameplate(app, name)\n        claims = [(row[\"side\"], row[\"claimed\"]) for row in side_rows]\n        self.assertIn((\"side1\", False), claims)\n        self.assertIn((\"side2\", True), claims)\n        self.assertIn((\"side3\", True), claims)\n\n        # releasing one side multiple times is ignored\n        app.release_nameplate(name, \"side1\", 5)\n        np_row, side_rows = self._nameplate(app, name)\n        claims = [(row[\"side\"], row[\"claimed\"]) for row in side_rows]\n        self.assertIn((\"side1\", False), claims)\n        self.assertIn((\"side2\", True), claims)\n        self.assertIn((\"side3\", True), claims)\n\n        # release the second side\n        app.release_nameplate(name, \"side2\", 6)\n        np_row, side_rows = self._nameplate(app, name)\n        claims = [(row[\"side\"], row[\"claimed\"]) for row in side_rows]\n        self.assertIn((\"side1\", False), claims)\n        self.assertIn((\"side2\", False), claims)\n        self.assertIn((\"side3\", True), claims)\n\n        # releasing the third side frees the nameplate, and adds usage\n        app.release_nameplate(name, \"side3\", 7)\n        np_row, side_rows = self._nameplate(app, name)\n        self.assertEqual(np_row, None)\n        usage = app._usage_db.execute(\"SELECT * FROM `nameplates`\").fetchone()\n        self.assertEqual(usage[\"app_id\"], \"appid\")\n        self.assertEqual(usage[\"started\"], 0)\n        self.assertEqual(usage[\"waiting_time\"], 3)\n        self.assertEqual(usage[\"total_time\"], 7)\n        self.assertEqual(usage[\"result\"], \"crowded\")\n\n\n    def test_mailbox(self):\n        app = self._server.get_app(\"appid\")\n        mailbox_id = \"mid\"\n        m1 = app.open_mailbox(mailbox_id, \"side1\", 0)\n\n        mb_row, side_rows = self._mailbox(app, mailbox_id)\n        self.assertEqual(len(side_rows), 1)\n        self.assertEqual(side_rows[0][\"side\"], \"side1\")\n        self.assertEqual(side_rows[0][\"added\"], 0)\n\n        # opening the same mailbox twice, by the same side, gets the same\n        # object, and does not update the \"added\" timestamp\n        self.assertIdentical(m1, app.open_mailbox(mailbox_id, \"side1\", 1))\n        mb_row, side_rows = self._mailbox(app, mailbox_id)\n        self.assertEqual(len(side_rows), 1)\n        self.assertEqual(side_rows[0][\"side\"], \"side1\")\n        self.assertEqual(side_rows[0][\"added\"], 0)\n\n        # opening a second side gets the same object, and adds a new claim\n        self.assertIdentical(m1, app.open_mailbox(mailbox_id, \"side2\", 2))\n        mb_row, side_rows = self._mailbox(app, mailbox_id)\n        self.assertEqual(len(side_rows), 2)\n        adds = [(row[\"side\"], row[\"added\"]) for row in side_rows]\n        self.assertIn((\"side1\", 0), adds)\n        self.assertIn((\"side2\", 2), adds)\n\n        # a third open marks it as crowded\n        self.assertRaises(CrowdedError,\n                          app.open_mailbox, mailbox_id, \"side3\", 3)\n        mb_row, side_rows = self._mailbox(app, mailbox_id)\n        self.assertEqual(len(side_rows), 3)\n        m1.close(\"side3\", \"company\", 4)\n\n        # closing a side that never claimed the mailbox is ignored\n        m1.close(\"side4\", \"mood\", 4)\n        mb_row, side_rows = self._mailbox(app, mailbox_id)\n        self.assertEqual(len(side_rows), 3)\n\n        # closing one side leaves the second claim\n        m1.close(\"side1\", \"mood\", 5)\n        mb_row, side_rows = self._mailbox(app, mailbox_id)\n        sides = [(row[\"side\"], row[\"opened\"], row[\"mood\"]) for row in side_rows]\n        self.assertIn((\"side1\", False, \"mood\"), sides)\n        self.assertIn((\"side2\", True, None), sides)\n        self.assertIn((\"side3\", False, \"company\"), sides)\n\n        # closing one side multiple times is ignored\n        m1.close(\"side1\", \"mood\", 6)\n        mb_row, side_rows = self._mailbox(app, mailbox_id)\n        sides = [(row[\"side\"], row[\"opened\"], row[\"mood\"]) for row in side_rows]\n        self.assertIn((\"side1\", False, \"mood\"), sides)\n        self.assertIn((\"side2\", True, None), sides)\n        self.assertIn((\"side3\", False, \"company\"), sides)\n\n        l1 = []; stop1 = []; stop1_f = lambda: stop1.append(True)\n        m1.add_listener(\"handle1\", l1.append, stop1_f)\n\n        # closing the second side frees the mailbox, and adds usage\n        m1.close(\"side2\", \"mood\", 7)\n        self.assertEqual(stop1, [True])\n\n        mb_row, side_rows = self._mailbox(app, mailbox_id)\n        self.assertEqual(mb_row, None)\n        usage = app._usage_db.execute(\"SELECT * FROM `mailboxes`\").fetchone()\n        self.assertEqual(usage[\"app_id\"], \"appid\")\n        self.assertEqual(usage[\"started\"], 0)\n        self.assertEqual(usage[\"waiting_time\"], 2)\n        self.assertEqual(usage[\"total_time\"], 7)\n        self.assertEqual(usage[\"result\"], \"crowded\")\n\n    def test_messages(self):\n        app = self._server.get_app(\"appid\")\n        mailbox_id = \"mid\"\n        m1 = app.open_mailbox(mailbox_id, \"side1\", 0)\n        m1.add_message(SidedMessage(side=\"side1\", phase=\"phase\",\n                                    body=\"body\", server_rx=1,\n                                    msg_id=\"msgid\"))\n        msgs = self._messages(app)\n        self.assertEqual(len(msgs), 1)\n        self.assertEqual(msgs[0][\"body\"], \"body\")\n\n        l1 = []; stop1 = []; stop1_f = lambda: stop1.append(True)\n        l2 = []; stop2 = []; stop2_f = lambda: stop2.append(True)\n        old = m1.add_listener(\"handle1\", l1.append, stop1_f)\n        self.assertEqual(len(old), 1)\n        self.assertEqual(old[0].side, \"side1\")\n        self.assertEqual(old[0].body, \"body\")\n\n        m1.add_message(SidedMessage(side=\"side1\", phase=\"phase2\",\n                                    body=\"body2\", server_rx=1,\n                                    msg_id=\"msgid\"))\n        self.assertEqual(len(l1), 1)\n        self.assertEqual(l1[0].body, \"body2\")\n        old = m1.add_listener(\"handle2\", l2.append, stop2_f)\n        self.assertEqual(len(old), 2)\n\n        m1.add_message(SidedMessage(side=\"side1\", phase=\"phase3\",\n                                    body=\"body3\", server_rx=1,\n                                    msg_id=\"msgid\"))\n        self.assertEqual(len(l1), 2)\n        self.assertEqual(l1[-1].body, \"body3\")\n        self.assertEqual(len(l2), 1)\n        self.assertEqual(l2[-1].body, \"body3\")\n\n        m1.remove_listener(\"handle1\")\n\n        m1.add_message(SidedMessage(side=\"side1\", phase=\"phase4\",\n                                    body=\"body4\", server_rx=1,\n                                    msg_id=\"msgid\"))\n        self.assertEqual(len(l1), 2)\n        self.assertEqual(l1[-1].body, \"body3\")\n        self.assertEqual(len(l2), 2)\n        self.assertEqual(l2[-1].body, \"body4\")\n\n        m1._shutdown()\n        self.assertEqual(stop1, [])\n        self.assertEqual(stop2, [True])\n\n        # message adds are not idempotent: clients filter duplicates\n        m1.add_message(SidedMessage(side=\"side1\", phase=\"phase\",\n                                    body=\"body\", server_rx=1,\n                                    msg_id=\"msgid\"))\n        msgs = self._messages(app)\n        self.assertEqual(len(msgs), 5)\n        self.assertEqual(msgs[-1][\"body\"], \"body\")\n\n    def test_early_close(self):\n        \"\"\"\n        One side opens a mailbox but closes it (explicitly) before any\n        other side joins.\n        \"\"\"\n        app = self._server.get_app(\"appid\")\n        name = app.allocate_nameplate(\"side1\", 42)\n        mbox = app.claim_nameplate(name, \"side1\", 0)\n        m = app.open_mailbox(mbox, \"side1\", 0)\n        m.close(\"side1\", \"mood\", 1)\n\n\nclass Prune(unittest.TestCase):\n\n    def _get_mailbox_updated(self, app, mbox_id):\n        row = app._db.execute(\"SELECT * FROM `mailboxes` WHERE\"\n                              \" `app_id`=? AND `id`=?\",\n                              (app._app_id, mbox_id)).fetchone()\n        return row[\"updated\"]\n\n    def test_update(self):\n        rv = make_server(create_channel_db(\":memory:\"))\n        app = rv.get_app(\"appid\")\n        mbox_id = \"mbox1\"\n        app.open_mailbox(mbox_id, \"side1\", 1)\n        self.assertEqual(self._get_mailbox_updated(app, mbox_id), 1)\n\n        mb = app.open_mailbox(mbox_id, \"side2\", 2)\n        self.assertEqual(self._get_mailbox_updated(app, mbox_id), 2)\n\n        sm = SidedMessage(\"side1\", \"phase\", \"body\", 3, \"msgid\")\n        mb.add_message(sm)\n        self.assertEqual(self._get_mailbox_updated(app, mbox_id), 3)\n\n    def test_apps(self):\n        rv = make_server(create_channel_db(\":memory:\"))\n        app = rv.get_app(\"appid\")\n        app.allocate_nameplate(\"side\", 121)\n        app.prune = mock.Mock()\n        rv.prune_all_apps(now=123, old=122)\n        self.assertEqual(app.prune.mock_calls, [mock.call(123, 122)])\n\n    def test_nameplates(self):\n        db = create_channel_db(\":memory:\")\n        rv = make_server(db, blur_usage=3600)\n\n        # timestamps <=50 are \"old\", >=51 are \"new\"\n        #OLD = \"old\"; NEW = \"new\"\n        #when = {OLD: 1, NEW: 60}\n        new_nameplates = set()\n\n        APPID = \"appid\"\n        app = rv.get_app(APPID)\n\n        # Exercise the first-vs-second newness tests\n        app.claim_nameplate(\"1\", \"side1\", 1)\n        app.claim_nameplate(\"2\", \"side1\", 1)\n        app.claim_nameplate(\"2\", \"side2\", 2)\n        app.claim_nameplate(\"3\", \"side1\", 60)\n        new_nameplates.add(\"3\")\n        app.claim_nameplate(\"4\", \"side1\", 1)\n        app.claim_nameplate(\"4\", \"side2\", 60)\n        new_nameplates.add(\"4\")\n        app.claim_nameplate(\"5\", \"side1\", 60)\n        app.claim_nameplate(\"5\", \"side2\", 61)\n        new_nameplates.add(\"5\")\n\n        rv.prune_all_apps(now=123, old=50)\n\n        nameplates = {row[\"name\"] for row in\n                          db.execute(\"SELECT * FROM `nameplates`\").fetchall()}\n        self.assertEqual(new_nameplates, nameplates)\n        mailboxes = {row[\"id\"] for row in\n                         db.execute(\"SELECT * FROM `mailboxes`\").fetchall()}\n        self.assertEqual(len(new_nameplates), len(mailboxes))\n\n        self.assertRaises(ValueError,\n                          app.claim_nameplate, \"letters\", \"side1\", 1)\n        long_but_ok = \"1234\"*10\n        app.claim_nameplate(long_but_ok, \"side1\", 1)\n        too_long = long_but_ok + \"5\"\n        self.assertRaises(ValueError,\n                          app.claim_nameplate, too_long, \"side1\", 1)\n\n    def test_mailboxes(self):\n        db = create_channel_db(\":memory:\")\n        rv = make_server(db, blur_usage=3600)\n\n        # timestamps <=50 are \"old\", >=51 are \"new\"\n        #OLD = \"old\"; NEW = \"new\"\n        #when = {OLD: 1, NEW: 60}\n        new_mailboxes = set()\n\n        APPID = \"appid\"\n        app = rv.get_app(APPID)\n\n        # Exercise the first-vs-second newness tests\n        app.open_mailbox(\"mb-11\", \"side1\", 1)\n        app.open_mailbox(\"mb-12\", \"side1\", 1)\n        app.open_mailbox(\"mb-12\", \"side2\", 2)\n        app.open_mailbox(\"mb-13\", \"side1\", 60)\n        new_mailboxes.add(\"mb-13\")\n        app.open_mailbox(\"mb-14\", \"side1\", 1)\n        app.open_mailbox(\"mb-14\", \"side2\", 60)\n        new_mailboxes.add(\"mb-14\")\n        app.open_mailbox(\"mb-15\", \"side1\", 60)\n        app.open_mailbox(\"mb-15\", \"side2\", 61)\n        new_mailboxes.add(\"mb-15\")\n\n        rv.prune_all_apps(now=123, old=50)\n\n        mailboxes = {row[\"id\"] for row in\n                         db.execute(\"SELECT * FROM `mailboxes`\").fetchall()}\n        self.assertEqual(new_mailboxes, mailboxes)\n\n    def test_lots(self):\n        OLD = \"old\"; NEW = \"new\"\n        for nameplate in [False, True]:\n            for mailbox in [OLD, NEW]:\n                for has_listeners in [False, True]:\n                    self.one(nameplate, mailbox, has_listeners)\n\n    def test_one(self):\n       # to debug specific problems found by test_lots\n       self.one(None, \"new\", False)\n\n    def one(self, nameplate, mailbox, has_listeners):\n        desc = (\"nameplate=%s, mailbox=%s, has_listeners=%s\" %\n                (nameplate, mailbox, has_listeners))\n        log.msg(desc)\n\n        db = create_channel_db(\":memory:\")\n        rv = make_server(db, blur_usage=3600)\n        APPID = \"appid\"\n        app = rv.get_app(APPID)\n\n        # timestamps <=50 are \"old\", >=51 are \"new\"\n        OLD = \"old\"; NEW = \"new\"\n        when = {OLD: 1, NEW: 60}\n        nameplate_survives = False\n        mailbox_survives = False\n\n        mbid = \"mbid\"\n        if nameplate:\n            mbid = app.claim_nameplate(npid, \"side1\", when[mailbox])\n        mb = app.open_mailbox(mbid, \"side1\", when[mailbox])\n\n        # the pruning algorithm doesn't care about the age of messages,\n        # because mailbox.updated is always updated each time we add a\n        # message\n        sm = SidedMessage(\"side1\", \"phase\", \"body\", when[mailbox], \"msgid\")\n        mb.add_message(sm)\n\n        if has_listeners:\n            mb.add_listener(\"handle\", None, None)\n\n        if (mailbox == NEW or has_listeners):\n            if nameplate:\n                nameplate_survives = True\n            mailbox_survives = True\n        messages_survive = mailbox_survives\n\n        rv.prune_all_apps(now=123, old=50)\n\n        nameplates = {row[\"name\"] for row in\n                          db.execute(\"SELECT * FROM `nameplates`\").fetchall()}\n        self.assertEqual(nameplate_survives, bool(nameplates),\n                         (\"nameplate\", nameplate_survives, nameplates, desc))\n\n        mailboxes = {row[\"id\"] for row in\n                         db.execute(\"SELECT * FROM `mailboxes`\").fetchall()}\n        self.assertEqual(mailbox_survives, bool(mailboxes),\n                         (\"mailbox\", mailbox_survives, mailboxes, desc))\n\n        messages = {row[\"msg_id\"] for row in\n                          db.execute(\"SELECT * FROM `messages`\").fetchall()}\n        self.assertEqual(messages_survive, bool(messages),\n                         (\"messages\", messages_survive, messages, desc))\n\n\nclass Summary(unittest.TestCase):\n    def test_mailbox(self):\n        app = AppNamespace(None, None, None, False, None, True)\n        # starts at time 1, maybe gets second open at time 3, closes at 5\n        def s(rows, pruned=False):\n            return app._summarize_mailbox(rows, 5, pruned)\n\n        rows = [dict(added=1)]\n        self.assertEqual(s(rows), Usage(1, None, 4, \"lonely\"))\n        rows = [dict(added=1, mood=\"lonely\")]\n        self.assertEqual(s(rows), Usage(1, None, 4, \"lonely\"))\n        rows = [dict(added=1, mood=\"errory\")]\n        self.assertEqual(s(rows), Usage(1, None, 4, \"errory\"))\n        rows = [dict(added=1, mood=None)]\n        self.assertEqual(s(rows, pruned=True), Usage(1, None, 4, \"pruney\"))\n        rows = [dict(added=1, mood=\"happy\")]\n        self.assertEqual(s(rows, pruned=True), Usage(1, None, 4, \"pruney\"))\n\n        rows = [dict(added=1, mood=\"happy\"), dict(added=3, mood=\"happy\")]\n        self.assertEqual(s(rows), Usage(1, 2, 4, \"happy\"))\n\n        rows = [dict(added=1, mood=\"errory\"), dict(added=3, mood=\"happy\")]\n        self.assertEqual(s(rows), Usage(1, 2, 4, \"errory\"))\n\n        rows = [dict(added=1, mood=\"happy\"), dict(added=3, mood=\"errory\")]\n        self.assertEqual(s(rows), Usage(1, 2, 4, \"errory\"))\n\n        rows = [dict(added=1, mood=\"scary\"), dict(added=3, mood=\"happy\")]\n        self.assertEqual(s(rows), Usage(1, 2, 4, \"scary\"))\n\n        rows = [dict(added=1, mood=\"scary\"), dict(added=3, mood=\"errory\")]\n        self.assertEqual(s(rows), Usage(1, 2, 4, \"scary\"))\n\n        rows = [dict(added=1, mood=\"happy\"), dict(added=3, mood=None)]\n        self.assertEqual(s(rows, pruned=True), Usage(1, 2, 4, \"pruney\"))\n        rows = [dict(added=1, mood=\"happy\"), dict(added=3, mood=\"happy\")]\n        self.assertEqual(s(rows, pruned=True), Usage(1, 2, 4, \"pruney\"))\n\n        rows = [dict(added=1), dict(added=3), dict(added=4)]\n        self.assertEqual(s(rows), Usage(1, 2, 4, \"crowded\"))\n\n        rows = [dict(added=1), dict(added=3), dict(added=4)]\n        self.assertEqual(s(rows, pruned=True), Usage(1, 2, 4, \"crowded\"))\n\n    def test_nameplate(self):\n        a = AppNamespace(None, None, None, False, None, True)\n        # starts at time 1, maybe gets second open at time 3, closes at 5\n        def s(rows, pruned=False):\n            return a._summarize_nameplate_usage(rows, 5, pruned)\n\n        rows = [dict(added=1)]\n        self.assertEqual(s(rows), Usage(1, None, 4, \"lonely\"))\n        rows = [dict(added=1), dict(added=3)]\n        self.assertEqual(s(rows), Usage(1, 2, 4, \"happy\"))\n\n        rows = [dict(added=1), dict(added=3)]\n        self.assertEqual(s(rows, pruned=True), Usage(1, 2, 4, \"pruney\"))\n\n        rows = [dict(added=1), dict(added=3), dict(added=4)]\n        self.assertEqual(s(rows), Usage(1, 2, 4, \"crowded\"))\n\n    def test_nameplate_disallowed(self):\n        db = create_channel_db(\":memory:\")\n        a = AppNamespace(db, None, None, False, \"some_app_id\", False)\n        a.allocate_nameplate(\"side1\", \"123\")\n        self.assertEqual([], a.get_nameplate_ids())\n\n    def test_nameplate_allowed(self):\n        db = create_channel_db(\":memory:\")\n        a = AppNamespace(db, None, None, False, \"some_app_id\", True)\n        np = a.allocate_nameplate(\"side1\", \"321\")\n        self.assertEqual({np}, a.get_nameplate_ids())\n\n    def test_blur(self):\n        db = create_channel_db(\":memory:\")\n        usage_db = create_usage_db(\":memory:\")\n        rv = make_server(db, blur_usage=3600, usage_db=usage_db)\n        APPID = \"appid\"\n        app = rv.get_app(APPID)\n        app.claim_nameplate(npid, \"side1\", 10) # start time is 10\n        rv.prune_all_apps(now=123, old=50)\n        # start time should be rounded to top of the hour (blur_usage=3600)\n        row = usage_db.execute(\"SELECT * FROM `nameplates`\").fetchone()\n        self.assertEqual(row[\"started\"], 0)\n\n        app = rv.get_app(APPID)\n        app.open_mailbox(\"mbid\", \"side1\", 20) # start time is 20\n        rv.prune_all_apps(now=123, old=50)\n        row = usage_db.execute(\"SELECT * FROM `mailboxes`\").fetchone()\n        self.assertEqual(row[\"started\"], 0)\n\n    def test_no_blur(self):\n        db = create_channel_db(\":memory:\")\n        usage_db = create_usage_db(\":memory:\")\n        rv = make_server(db, blur_usage=None, usage_db=usage_db)\n        APPID = \"appid\"\n        app = rv.get_app(APPID)\n        app.claim_nameplate(npid, \"side1\", 10) # start time is 10\n        rv.prune_all_apps(now=123, old=50)\n        row = usage_db.execute(\"SELECT * FROM `nameplates`\").fetchone()\n        self.assertEqual(row[\"started\"], 10)\n\n        usage_db.execute(\"DELETE FROM `mailboxes`\")\n        usage_db.commit()\n        app = rv.get_app(APPID)\n        app.open_mailbox(\"mbid\", \"side1\", 20) # start time is 20\n        rv.prune_all_apps(now=123, old=50)\n        row = usage_db.execute(\"SELECT * FROM `mailboxes`\").fetchone()\n        self.assertEqual(row[\"started\"], 20)\n\n## class DumpStats(unittest.TestCase):\n##     def test_nostats(self):\n##         rs = easy_relay()\n##         # with no ._stats_file, this should do nothing\n##         rs.dump_stats(1, 1)\n\n##     def test_empty(self):\n##         basedir = self.mktemp()\n##         os.mkdir(basedir)\n##         fn = os.path.join(basedir, \"stats.json\")\n##         rs = easy_relay(stats_file=fn)\n##         now = 1234\n##         validity = 500\n##         rs.dump_stats(now, validity)\n##         with open(fn, \"rb\") as f:\n##             data_bytes = f.read()\n##         data = json.loads(data_bytes.decode(\"utf-8\"))\n##         self.assertEqual(data[\"created\"], now)\n##         self.assertEqual(data[\"valid_until\"], now+validity)\n##         self.assertEqual(data[\"rendezvous\"][\"all_time\"][\"mailboxes_total\"], 0)\n\nclass Startup(unittest.TestCase):\n    @mock.patch('wormhole_mailbox_server.server.log')\n    def test_empty(self, fake_log):\n        db = create_channel_db(\":memory:\")\n        s = make_server(db, allow_list=False)\n        s.startService()\n        try:\n            logs = '\\n'.join([call[1][0] for call in fake_log.mock_calls])\n            self.assertIn('listing of allocated nameplates disallowed', logs)\n        finally:\n            s.stopService()\n\n    @mock.patch('wormhole_mailbox_server.server.log')\n    def test_allow_list(self, fake_log):\n        db = create_channel_db(\":memory:\")\n        s = make_server(db, allow_list=True)\n        s.startService()\n        try:\n            logs = '\\n'.join([call[1][0] for call in fake_log.mock_calls])\n            self.assertNotIn('listing of allocated nameplates disallowed', logs)\n        finally:\n            s.stopService()\n\n    @mock.patch('wormhole_mailbox_server.server.log')\n    def test_blur_usage(self, fake_log):\n        db = create_channel_db(\":memory:\")\n        s = make_server(db, blur_usage=60, allow_list=True)\n        s.startService()\n        try:\n            logs = '\\n'.join([call[1][0] for call in fake_log.mock_calls])\n            self.assertNotIn('listing of allocated nameplates disallowed', logs)\n            self.assertIn('blurring access times to 60 seconds', logs)\n        finally:\n            s.stopService()\n\nclass MakeServer(unittest.TestCase):\n    def test_welcome_empty(self):\n        db = create_channel_db(\":memory:\")\n        s = make_server(db)\n        self.assertEqual(s.get_welcome(), {})\n\n    def test_welcome_error(self):\n        db = create_channel_db(\":memory:\")\n        s = make_server(db, signal_error=\"error!\")\n        self.assertEqual(\n            s.get_welcome(),\n            {\n                \"error\": \"error!\",\n            }\n        )\n\n    def test_welcome_advertise_version(self):\n        db = create_channel_db(\":memory:\")\n        s = make_server(db, advertise_version=\"version\")\n        self.assertEqual(\n            s.get_welcome(),\n            {\n                \"current_cli_version\": \"version\",\n            }\n        )\n\n    def test_welcome_message_of_the_day(self):\n        db = create_channel_db(\":memory:\")\n        s = make_server(db, welcome_motd=\"hello world\")\n        self.assertEqual(\n            s.get_welcome(),\n            {\n                \"motd\": \"hello world\",\n            }\n        )\n\n# exercise _find_available_nameplate_id failing\n# exercise CrowdedError\n# exercise double free_mailbox\n# exercise _summarize_mailbox = quiet (0 sides)\n# exercise AppNamespace._shutdown\n#  so Server.stopService\n## test blur_usage/not on Server\n## test make_server(signal_error=)\n## exercise dump_stats (with/without usagedb)\n\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_service.py",
    "content": "from twisted.trial import unittest\nfrom unittest import mock\nfrom twisted.application.service import MultiService\nfrom .. import server_tap\n\nclass Service(unittest.TestCase):\n    def test_defaults(self):\n        o = server_tap.Options()\n        o.parseOptions([])\n        cdb = object()\n        udb = object()\n        r = mock.Mock()\n        ws = object()\n        with mock.patch(\"wormhole_mailbox_server.server_tap.create_or_upgrade_channel_db\", return_value=cdb) as ccdb:\n            with mock.patch(\"wormhole_mailbox_server.server_tap.create_or_upgrade_usage_db\", return_value=udb) as ccub:\n                with mock.patch(\"wormhole_mailbox_server.server_tap.make_server\", return_value=r) as ms:\n                    with mock.patch(\"wormhole_mailbox_server.server_tap.make_web_server\", return_value=ws) as mws:\n                        s = server_tap.makeService(o)\n        self.assertEqual(ccdb.mock_calls, [mock.call(\"relay.sqlite\")])\n        self.assertEqual(ccub.mock_calls, [mock.call(None)])\n        self.assertEqual(ms.mock_calls, [mock.call(cdb, allow_list=True,\n                                                   advertise_version=None,\n                                                   signal_error=None,\n                                                   welcome_motd=None,\n                                                   blur_usage=None,\n                                                   usage_db=udb,\n                                                   log_file=None)])\n        self.assertEqual(mws.mock_calls, [mock.call(r, True, [])])\n        self.assertIsInstance(s, MultiService)\n        self.assertEqual(len(r.mock_calls), 1) # setServiceParent\n\n    def test_log_fd(self):\n        o = server_tap.Options()\n        o.parseOptions([\"--log-fd=99\"])\n        fd = object()\n        cdb = object()\n        udb = object()\n        r = mock.Mock()\n        ws = object()\n        with mock.patch(\"wormhole_mailbox_server.server_tap.create_or_upgrade_channel_db\", return_value=cdb):\n            with mock.patch(\"wormhole_mailbox_server.server_tap.create_or_upgrade_usage_db\", return_value=udb):\n                with mock.patch(\"wormhole_mailbox_server.server_tap.make_server\", return_value=r) as ms:\n                    with mock.patch(\"wormhole_mailbox_server.server_tap.make_web_server\", return_value=ws):\n                        with mock.patch(\"wormhole_mailbox_server.server_tap.os.fdopen\",\n                                        return_value=fd) as f:\n                            server_tap.makeService(o)\n        self.assertEqual(f.mock_calls, [mock.call(99, \"w\")])\n        self.assertEqual(ms.mock_calls, [mock.call(cdb, allow_list=True,\n                                                   advertise_version=None,\n                                                   signal_error=None,\n                                                   welcome_motd=None,\n                                                   blur_usage=None,\n                                                   usage_db=udb,\n                                                   log_file=fd)])\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_stats.py",
    "content": "#import io, json\nfrom twisted.trial import unittest\nfrom ..database import create_channel_db, create_usage_db\nfrom ..server import make_server, CrowdedError\n\nnp1 = \"1\"\n\nclass _Make:\n    def make(self, blur_usage=None, with_usage_db=True):\n        self._cdb = create_channel_db(\":memory:\")\n        db = create_usage_db(\":memory:\") if with_usage_db else None\n        s = make_server(self._cdb, usage_db=db, blur_usage=blur_usage)\n        app = s.get_app(\"appid\")\n        return s, db, app\n\nclass Current(_Make, unittest.TestCase):\n    def test_current_no_mailboxes(self):\n        s, db, app = self.make()\n        s.dump_stats(456, rebooted=451)\n        self.assertEqual(db.execute(\"SELECT * FROM `current`\").fetchall(),\n                         [dict(rebooted=451, updated=456, blur_time=None,\n                               connections_websocket=0),\n                          ])\n\n    def test_current_no_listeners(self):\n        s, db, app = self.make()\n        app.open_mailbox(\"m1\", \"s1\", 1)\n        s.dump_stats(456, rebooted=451)\n        self.assertEqual(db.execute(\"SELECT * FROM `current`\").fetchall(),\n                         [dict(rebooted=451, updated=456, blur_time=None,\n                               connections_websocket=0),\n                          ])\n\n    def test_current_one_listener(self):\n        s, db, app = self.make()\n        mbox = app.open_mailbox(\"m1\", \"s1\", 1)\n        mbox.add_listener(\"h1\", lambda sm: None, lambda: None)\n        s.dump_stats(456, rebooted=451)\n        self.assertEqual(db.execute(\"SELECT * FROM `current`\").fetchall(),\n                         [dict(rebooted=451, updated=456, blur_time=None,\n                               connections_websocket=1),\n                          ])\n\nclass ClientVersion(_Make, unittest.TestCase):\n    def test_add_version(self):\n        s, db, app = self.make()\n        app.log_client_version(451, \"side1\", (\"python\", \"1.2.3\"))\n        self.assertEqual(db.execute(\"SELECT * FROM `client_versions`\").fetchall(),\n                         [dict(app_id=\"appid\", connect_time=451, side=\"side1\",\n                               implementation=\"python\", version=\"1.2.3\")])\n\n    def test_add_version_extra_fields(self):\n        s, db, app = self.make()\n        app.log_client_version(451, \"side1\", (\"python\", \"1.2.3\", \"extra\"))\n        self.assertEqual(db.execute(\"SELECT * FROM `client_versions`\").fetchall(),\n                         [dict(app_id=\"appid\", connect_time=451, side=\"side1\",\n                               implementation=\"python\", version=\"1.2.3\")])\n\n    def test_blur(self):\n        s, db, app = self.make(blur_usage=100)\n        app.log_client_version(451, \"side1\", (\"python\", \"1.2.3\"))\n        self.assertEqual(db.execute(\"SELECT * FROM `client_versions`\").fetchall(),\n                         [dict(app_id=\"appid\", connect_time=400, side=\"side1\",\n                               implementation=\"python\", version=\"1.2.3\")])\n\n    def test_no_usage_db(self):\n        s, db, app = self.make(with_usage_db=False)\n        app.log_client_version(451, \"side1\", (\"python\", \"1.2.3\"))\n\nclass Nameplate(_Make, unittest.TestCase):\n    def test_nameplate_happy(self):\n        s, db, app = self.make()\n        app.claim_nameplate(np1, \"s1\", 1)\n        app.claim_nameplate(np1, \"s2\", 3)\n        app.release_nameplate(np1, \"s1\", 6)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [])\n        app.release_nameplate(np1, \"s2\", 10)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [dict(app_id=\"appid\", result=\"happy\",\n                               started=1, waiting_time=2, total_time=9)])\n\n    def test_nameplate_lonely(self):\n        s, db, app = self.make()\n        app.claim_nameplate(np1, \"s1\", 1)\n        app.release_nameplate(np1, \"s1\", 6)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [dict(app_id=\"appid\", result=\"lonely\",\n                               started=1, waiting_time=None, total_time=5)])\n\n    def test_nameplate_pruney(self):\n        s, db, app = self.make()\n        app.claim_nameplate(np1, \"s1\", 1)\n        app.prune(10, 5) # prune at t=10, anything earlier than 5 is \"old\"\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [dict(app_id=\"appid\", result=\"pruney\",\n                               started=1, waiting_time=None, total_time=9)])\n\n    def test_nameplate_crowded(self):\n        s, db, app = self.make()\n        app.claim_nameplate(np1, \"s1\", 1)\n        app.claim_nameplate(np1, \"s2\", 2)\n        with self.assertRaises(CrowdedError):\n            app.claim_nameplate(np1, \"s3\", 3)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [])\n        app.release_nameplate(np1, \"s1\", 4)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [])\n        app.release_nameplate(np1, \"s2\", 5)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [])\n        #print(self._cdb.execute(\"SELECT * FROM `nameplates`\").fetchall())\n        #print(self._cdb.execute(\"SELECT * FROM `nameplate_sides`\").fetchall())\n        # TODO: to get \"crowded\", we need all three sides to release the\n        # nameplate, even though the third side threw CrowdedError and thus\n        # probably doesn't think it has a claim\n        app.release_nameplate(np1, \"s3\", 6)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [dict(app_id=\"appid\", result=\"crowded\",\n                               started=1, waiting_time=1, total_time=5)])\n\n    def test_nameplate_crowded_pruned(self):\n        s, db, app = self.make()\n        app.claim_nameplate(np1, \"s1\", 1)\n        app.claim_nameplate(np1, \"s2\", 2)\n        with self.assertRaises(CrowdedError):\n            app.claim_nameplate(np1, \"s3\", 3)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [])\n        app.prune(10, 5)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [dict(app_id=\"appid\", result=\"crowded\",\n                               started=1, waiting_time=1, total_time=9)])\n\n    def test_no_db(self):\n        s, db, app = self.make(with_usage_db=False)\n        app.claim_nameplate(np1, \"s1\", 1)\n        app.release_nameplate(np1, \"s1\", 6)\n        s.dump_stats(3, 1)\n\n    def test_nameplate_happy_blur_usage(self):\n        s, db, app = self.make(blur_usage=20)\n        app.claim_nameplate(np1, \"s1\", 21)\n        app.claim_nameplate(np1, \"s2\", 23)\n        app.release_nameplate(np1, \"s1\", 26)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [])\n        app.release_nameplate(np1, \"s2\", 30)\n        self.assertEqual(db.execute(\"SELECT * FROM `nameplates`\").fetchall(),\n                         [dict(app_id=\"appid\", result=\"happy\",\n                               started=20, waiting_time=2, total_time=9)])\n\nclass Mailbox(_Make, unittest.TestCase):\n    def test_mailbox_prune_quiet(self):\n        s, db, app = self.make()\n        app.claim_nameplate(np1, \"s1\", 1)\n        app.release_nameplate(np1, \"s1\", 2)\n        app.prune(10, 5)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"pruney\",\n                               started=1, waiting_time=None, total_time=9)])\n\n    def test_mailbox_lonely(self):\n        s, db, app = self.make()\n        mid = app.claim_nameplate(np1, \"s1\", 1)\n        mbox = app.open_mailbox(mid, \"s1\", 2)\n        app.release_nameplate(np1, \"s1\", 3)\n        mbox.close(\"s1\", \"mood-ignored\", 4)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"lonely\",\n                               started=1, waiting_time=None, total_time=3)])\n\n    def test_mailbox_happy(self):\n        s, db, app = self.make()\n        mid = app.claim_nameplate(np1, \"s1\", 1)\n        mbox1 = app.open_mailbox(mid, \"s1\", 2)\n        app.release_nameplate(np1, \"s1\", 3)\n        mbox2 = app.open_mailbox(mid, \"s2\", 4)\n        mbox1.close(\"s1\", \"happy\", 5)\n        mbox2.close(\"s2\", \"happy\", 6)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"happy\",\n                               started=1, waiting_time=3, total_time=5)])\n\n    def test_mailbox_happy_blur_usage(self):\n        s, db, app = self.make(blur_usage=20)\n        mid = app.claim_nameplate(np1, \"s1\", 21)\n        mbox1 = app.open_mailbox(mid, \"s1\", 22)\n        app.release_nameplate(np1, \"s1\", 23)\n        mbox2 = app.open_mailbox(mid, \"s2\", 24)\n        mbox1.close(\"s1\", \"happy\", 25)\n        mbox2.close(\"s2\", \"happy\", 26)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"happy\",\n                               started=20, waiting_time=3, total_time=5)])\n\n    def test_mailbox_lonely_connected(self):\n        # I don't think this could actually happen. It requires both sides to\n        # connect, but then at least one side says they're lonely when they\n        # close.\n        s, db, app = self.make()\n        mid = app.claim_nameplate(np1, \"s1\", 1)\n        mbox1 = app.open_mailbox(mid, \"s1\", 2)\n        app.release_nameplate(np1, \"s1\", 3)\n        mbox2 = app.open_mailbox(mid, \"s2\", 4)\n        mbox1.close(\"s1\", \"lonely\", 5)\n        mbox2.close(\"s2\", \"happy\", 6)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"lonely\",\n                               started=1, waiting_time=3, total_time=5)])\n\n    def test_mailbox_scary(self):\n        s, db, app = self.make()\n        mid = app.claim_nameplate(np1, \"s1\", 1)\n        mbox1 = app.open_mailbox(mid, \"s1\", 2)\n        app.release_nameplate(np1, \"s1\", 3)\n        mbox2 = app.open_mailbox(mid, \"s2\", 4)\n        mbox1.close(\"s1\", \"scary\", 5)\n        mbox2.close(\"s2\", \"happy\", 6)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"scary\",\n                               started=1, waiting_time=3, total_time=5)])\n\n    def test_mailbox_errory(self):\n        s, db, app = self.make()\n        mid = app.claim_nameplate(np1, \"s1\", 1)\n        mbox1 = app.open_mailbox(mid, \"s1\", 2)\n        app.release_nameplate(np1, \"s1\", 3)\n        mbox2 = app.open_mailbox(mid, \"s2\", 4)\n        mbox1.close(\"s1\", \"errory\", 5)\n        mbox2.close(\"s2\", \"happy\", 6)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"errory\",\n                               started=1, waiting_time=3, total_time=5)])\n\n    def test_mailbox_errory_scary(self):\n        s, db, app = self.make()\n        mid = app.claim_nameplate(np1, \"s1\", 1)\n        mbox1 = app.open_mailbox(mid, \"s1\", 2)\n        app.release_nameplate(np1, \"s1\", 3)\n        mbox2 = app.open_mailbox(mid, \"s2\", 4)\n        mbox1.close(\"s1\", \"errory\", 5)\n        mbox2.close(\"s2\", \"scary\", 6)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"scary\",\n                               started=1, waiting_time=3, total_time=5)])\n\n    def test_mailbox_crowded(self):\n        s, db, app = self.make()\n        mid = app.claim_nameplate(np1, \"s1\", 1)\n        mbox1 = app.open_mailbox(mid, \"s1\", 2)\n        app.release_nameplate(np1, \"s1\", 3)\n        mbox2 = app.open_mailbox(mid, \"s2\", 4)\n        with self.assertRaises(CrowdedError):\n            app.open_mailbox(mid, \"s3\", 5)\n        mbox1.close(\"s1\", \"happy\", 6)\n        mbox2.close(\"s2\", \"happy\", 7)\n        # again, not realistic\n        mbox2.close(\"s3\", \"happy\", 8)\n        self.assertEqual(db.execute(\"SELECT * FROM `mailboxes`\").fetchall(),\n                         [dict(app_id=\"appid\", for_nameplate=1, result=\"crowded\",\n                               started=1, waiting_time=3, total_time=7)])\n\n## class LogToStdout(unittest.TestCase):\n##     def test_log(self):\n##         # emit lines of JSON to log_file, if set\n##         log_file = io.StringIO()\n##         t = Transit(blur_usage=None, log_file=log_file, usage_db=None)\n##         t.recordUsage(started=123, result=\"happy\", total_bytes=100,\n##                       total_time=10, waiting_time=2)\n##         self.assertEqual(json.loads(log_file.getvalue()),\n##                          {\"started\": 123, \"total_time\": 10,\n##                           \"waiting_time\": 2, \"total_bytes\": 100,\n##                           \"mood\": \"happy\"})\n\n##     def test_log_blurred(self):\n##         # if blurring is enabled, timestamps should be rounded to the\n##         # requested amount, and sizes should be rounded up too\n##         log_file = io.StringIO()\n##         t = Transit(blur_usage=60, log_file=log_file, usage_db=None)\n##         t.recordUsage(started=123, result=\"happy\", total_bytes=11999,\n##                       total_time=10, waiting_time=2)\n##         self.assertEqual(json.loads(log_file.getvalue()),\n##                          {\"started\": 120, \"total_time\": 10,\n##                           \"waiting_time\": 2, \"total_bytes\": 20000,\n##                           \"mood\": \"happy\"})\n\n##     def test_do_not_log(self):\n##         t = Transit(blur_usage=60, log_file=None, usage_db=None)\n##         t.recordUsage(started=123, result=\"happy\", total_bytes=11999,\n##                       total_time=10, waiting_time=2)\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_util.py",
    "content": "import unicodedata\nfrom twisted.trial import unittest\nfrom .. import util\n\nclass Utils(unittest.TestCase):\n    def test_to_bytes(self):\n        b = util.to_bytes(\"abc\")\n        self.assertIsInstance(b, bytes)\n        self.assertEqual(b, b\"abc\")\n\n        A = unicodedata.lookup(\"LATIN SMALL LETTER A WITH DIAERESIS\")\n        b = util.to_bytes(A + \"bc\")\n        self.assertIsInstance(b, bytes)\n        self.assertEqual(b, b\"\\xc3\\xa4\\x62\\x63\")\n\n    def test_bytes_to_hexstr(self):\n        b = b\"\\x00\\x45\\x91\\xfe\\xff\"\n        hexstr = util.bytes_to_hexstr(b)\n        self.assertIsInstance(hexstr, str)\n        self.assertEqual(hexstr, \"004591feff\")\n\n    def test_hexstr_to_bytes(self):\n        hexstr = \"004591feff\"\n        b = util.hexstr_to_bytes(hexstr)\n        hexstr = util.bytes_to_hexstr(b)\n        self.assertIsInstance(b, bytes)\n        self.assertEqual(b, b\"\\x00\\x45\\x91\\xfe\\xff\")\n\n    def test_dict_to_bytes(self):\n        d = {\"a\": \"b\"}\n        b = util.dict_to_bytes(d)\n        self.assertIsInstance(b, bytes)\n        self.assertEqual(b, b'{\"a\": \"b\"}')\n\n    def test_bytes_to_dict(self):\n        b = b'{\"a\": \"b\", \"c\": 2}'\n        d = util.bytes_to_dict(b)\n        self.assertIsInstance(d, dict)\n        self.assertEqual(d, {\"a\": \"b\", \"c\": 2})\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_web.py",
    "content": "import io, time\nfrom unittest import mock\nimport treq\nfrom twisted.trial import unittest\nfrom twisted.internet import defer, reactor\nfrom twisted.internet.defer import inlineCallbacks\nfrom ..web import make_web_server\nfrom ..server import SidedMessage\nfrom ..database import create_or_upgrade_usage_db\nfrom .common import ServerBase, _Util\nfrom .ws_client import WSFactory\n\nnp1 = \"1\"\nnp2 = \"2\"\n\nclass WebSocketProtocolOptions(unittest.TestCase):\n    @mock.patch('wormhole_mailbox_server.web.WebSocketServerFactory')\n    def test_set(self, fake_factory):\n        make_web_server(None, False,\n                        websocket_protocol_options=[ (\"foo\", \"bar\"), ],\n                        )\n        self.assertEqual(\n            mock.call().setProtocolOptions(foo=\"bar\"),\n            fake_factory.mock_calls[1],\n        )\n\nclass LogRequests(ServerBase, unittest.TestCase):\n    def setUp(self):\n        self._clients = []\n\n    def tearDown(self):\n        for c in self._clients:\n            c.transport.loseConnection()\n        return ServerBase.tearDown(self)\n\n    @inlineCallbacks\n    def make_client(self):\n        f = WSFactory(self.relayurl)\n        f.d = defer.Deferred()\n        reactor.connectTCP(\"127.0.0.1\", self.rdv_ws_port, f)\n        c = yield f.d\n        self._clients.append(c)\n        return c\n\n    @inlineCallbacks\n    def test_log_http(self):\n        yield self._setup_relay(do_listen=True, web_log_requests=True)\n        # check the HTTP log\n        fakelog = io.BytesIO()\n        self._site.logFile = fakelog\n        yield treq.get(\"http://127.0.0.1:%d/\" % self.rdv_ws_port,\n                       persistent=False)\n        lines = fakelog.getvalue().splitlines()\n        self.assertEqual(len(lines), 1, lines)\n\n    @inlineCallbacks\n    def test_log_websocket(self):\n        yield self._setup_relay(do_listen=True, web_log_requests=True)\n        # now check the twisted log for websocket connect messages\n        with mock.patch(\"wormhole_mailbox_server.server_websocket.log.msg\") as l:\n            c1 = yield self.make_client()\n            yield c1.next_non_ack()\n            # the actual message includes the TCP port number of the client\n            client_port = self._clients[0].transport.getHost().port\n            expected = \"ws client connecting: tcp4:127.0.0.1:%d\" % client_port\n            self.assertEqual(l.mock_calls, [mock.call(expected)])\n\n    @inlineCallbacks\n    def test_no_log_http(self):\n        yield self._setup_relay(do_listen=True, web_log_requests=False)\n        # check the HTTP log\n        fakelog = io.BytesIO()\n        self._site.logFile = fakelog\n        yield treq.get(\"http://127.0.0.1:%d/\" % self.rdv_ws_port,\n                       persistent=False)\n        lines = fakelog.getvalue().splitlines()\n        self.assertEqual(len(lines), 0, lines)\n\n    @inlineCallbacks\n    def test_no_log_websocket(self):\n        yield self._setup_relay(do_listen=True,\n                                blur_usage=60, web_log_requests=True)\n        # now check the twisted log for websocket connect messages\n        with mock.patch(\"wormhole_mailbox_server.server_websocket.log.msg\") as l:\n            c1 = yield self.make_client()\n            yield c1.next_non_ack()\n            self.assertEqual(l.mock_calls, [])\n\n\nclass WebSocketAPI(_Util, ServerBase, unittest.TestCase):\n    @inlineCallbacks\n    def setUp(self):\n        self._lp = None\n        self._clients = []\n        self._usage_db = usage_db = create_or_upgrade_usage_db(\":memory:\")\n        yield self._setup_relay(do_listen=True,\n                                advertise_version=\"advertised.version\",\n                                usage_db=usage_db)\n\n    def tearDown(self):\n        for c in self._clients:\n            c.transport.loseConnection()\n        return ServerBase.tearDown(self)\n\n    @inlineCallbacks\n    def make_client(self):\n        f = WSFactory(self.relayurl)\n        f.d = defer.Deferred()\n        reactor.connectTCP(\"127.0.0.1\", self.rdv_ws_port, f)\n        c = yield f.d\n        self._clients.append(c)\n        return c\n\n    def check_welcome(self, data):\n        self.failUnlessIn(\"welcome\", data)\n        self.assertEqual(data[\"welcome\"][\"current_cli_version\"], \"advertised.version\")\n\n    @inlineCallbacks\n    def test_welcome(self):\n        c1 = yield self.make_client()\n        msg = yield c1.next_non_ack()\n        self.check_welcome(msg)\n        self.assertEqual(self._server._apps, {})\n\n    @inlineCallbacks\n    def test_bind(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n\n        c1.send(\"bind\", appid=\"appid\") # missing side=\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"bind requires 'side'\")\n\n        c1.send(\"bind\", side=\"side\") # missing appid=\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"bind requires 'appid'\")\n\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        yield c1.sync()\n        self.assertEqual(list(self._server._apps.keys()), [\"appid\"])\n\n        c1.send(\"bind\", appid=\"appid\", side=\"side\") # duplicate\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"already bound\")\n\n        c1.send_notype(other=\"misc\") # missing 'type'\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"missing 'type'\")\n\n        c1.send(\"___unknown\") # unknown type\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"unknown type\")\n\n        c1.send(\"ping\") # missing 'ping'\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"ping requires 'ping'\")\n\n    @inlineCallbacks\n    def test_bind_with_client_version(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n\n        c1.send(\"bind\", appid=\"appid\", side=\"side\",\n                client_version=(\"python\", \"1.2.3\"))\n        yield c1.sync()\n        self.assertEqual(list(self._server._apps.keys()), [\"appid\"])\n        v = self._usage_db.execute(\"SELECT * FROM `client_versions`\").fetchall()\n        self.assertEqual(v[0][\"app_id\"], \"appid\")\n        self.assertEqual(v[0][\"side\"], \"side\")\n        self.assertEqual(v[0][\"implementation\"], \"python\")\n        self.assertEqual(v[0][\"version\"], \"1.2.3\")\n\n    @inlineCallbacks\n    def test_bind_without_client_version(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        yield c1.sync()\n        self.assertEqual(list(self._server._apps.keys()), [\"appid\"])\n        v = self._usage_db.execute(\"SELECT * FROM `client_versions`\").fetchall()\n        self.assertEqual(v[0][\"app_id\"], \"appid\")\n        self.assertEqual(v[0][\"side\"], \"side\")\n        self.assertEqual(v[0][\"implementation\"], None)\n        self.assertEqual(v[0][\"version\"], None)\n\n    @inlineCallbacks\n    def test_bind_with_client_version_extra_junk(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n\n        c1.send(\"bind\", appid=\"appid\", side=\"side\",\n                client_version=(\"python\", \"1.2.3\", \"extra ignore me\"))\n        yield c1.sync()\n        self.assertEqual(list(self._server._apps.keys()), [\"appid\"])\n        v = self._usage_db.execute(\"SELECT * FROM `client_versions`\").fetchall()\n        self.assertEqual(v[0][\"app_id\"], \"appid\")\n        self.assertEqual(v[0][\"side\"], \"side\")\n        self.assertEqual(v[0][\"implementation\"], \"python\")\n        self.assertEqual(v[0][\"version\"], \"1.2.3\")\n\n    @inlineCallbacks\n    def test_list(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n\n        c1.send(\"list\") # too early, must bind first\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"must bind first\")\n\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        c1.send(\"list\")\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"nameplates\")\n        self.assertEqual(m[\"nameplates\"], [])\n\n        app = self._server.get_app(\"appid\")\n        nameplate_id1 = app.allocate_nameplate(\"side\", 0)\n        app.claim_nameplate(np2, \"side\", 0)\n\n        c1.send(\"list\")\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"nameplates\")\n        nids = set()\n        for n in m[\"nameplates\"]:\n            self.assertEqual(type(n), dict)\n            self.assertEqual(list(n.keys()), [\"id\"])\n            nids.add(n[\"id\"])\n        self.assertEqual(nids, {nameplate_id1, np2})\n\n    @inlineCallbacks\n    def test_allocate(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n\n        c1.send(\"allocate\") # too early, must bind first\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"must bind first\")\n\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n        c1.send(\"allocate\")\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"allocated\")\n        name = m[\"nameplate\"]\n\n        nids = app.get_nameplate_ids()\n        self.assertEqual(len(nids), 1)\n        self.assertEqual(name, list(nids)[0])\n\n        c1.send(\"allocate\")\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"],\n                         \"you already allocated one, don't be greedy\")\n\n        c1.send(\"claim\", nameplate=name) # allocate+claim is ok\n        yield c1.sync()\n        np_row, side_rows = self._nameplate(app, name)\n        self.assertEqual(len(side_rows), 1)\n        self.assertEqual(side_rows[0][\"side\"], \"side\")\n\n    @inlineCallbacks\n    def test_claim(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        c1.send(\"claim\") # missing nameplate=\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"claim requires 'nameplate'\")\n\n        c1.send(\"claim\", nameplate=np1)\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"claimed\")\n        mailbox_id = m[\"mailbox\"]\n        self.assertEqual(type(mailbox_id), str)\n\n        c1.send(\"claim\", nameplate=np1)\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\", err)\n        self.assertEqual(err[\"error\"], \"only one claim per connection\")\n\n        nids = app.get_nameplate_ids()\n        self.assertEqual(len(nids), 1)\n        self.assertEqual(np1, list(nids)[0])\n        np_row, side_rows = self._nameplate(app, np1)\n        self.assertEqual(len(side_rows), 1)\n        self.assertEqual(side_rows[0][\"side\"], \"side\")\n\n        # claiming a nameplate assigns a random mailbox id and creates the\n        # mailbox row\n        mailboxes = app._db.execute(\"SELECT * FROM `mailboxes`\"\n                                    \" WHERE `app_id`='appid'\").fetchall()\n        self.assertEqual(len(mailboxes), 1)\n\n    @inlineCallbacks\n    def test_claim_crowded(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        app.claim_nameplate(np1, \"side1\", 0)\n        app.claim_nameplate(np1, \"side2\", 0)\n\n        # the third claim will signal crowding\n        c1.send(\"claim\", nameplate=np1)\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"crowded\")\n\n    @inlineCallbacks\n    def test_release(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        app.claim_nameplate(np1, \"side2\", 0)\n\n        c1.send(\"release\") # didn't do claim first\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"],\n                         \"release without nameplate must follow claim\")\n\n        c1.send(\"claim\", nameplate=np1)\n        yield c1.next_non_ack()\n\n        c1.send(\"release\")\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"released\", m)\n\n        np_row, side_rows = self._nameplate(app, np1)\n        claims = [(row[\"side\"], row[\"claimed\"]) for row in side_rows]\n        self.assertIn((\"side\", False), claims)\n        self.assertIn((\"side2\", True), claims)\n\n        c1.send(\"release\") # no longer claimed\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"only one release per connection\")\n\n    @inlineCallbacks\n    def test_release_named(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n\n        c1.send(\"claim\", nameplate=np1)\n        yield c1.next_non_ack()\n\n        c1.send(\"release\", nameplate=np1)\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"released\", m)\n\n    @inlineCallbacks\n    def test_release_named_ignored(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n\n        c1.send(\"release\", nameplate=np1) # didn't do claim first, ignored\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"released\", m)\n\n    @inlineCallbacks\n    def test_release_named_mismatch(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n\n        c1.send(\"claim\", nameplate=np1)\n        yield c1.next_non_ack()\n\n        c1.send(\"release\", nameplate=np2) # mismatching nameplate\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"],\n                         \"release and claim must use same nameplate\")\n\n    @inlineCallbacks\n    def test_open(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        c1.send(\"open\") # missing mailbox=\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"open requires 'mailbox'\")\n\n        mb1 = app.open_mailbox(\"mb1\", \"side2\", 0)\n        mb1.add_message(SidedMessage(side=\"side2\", phase=\"phase\",\n                                     body=\"body\", server_rx=0,\n                                     msg_id=\"msgid\"))\n\n        c1.send(\"open\", mailbox=\"mb1\")\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"message\")\n        self.assertEqual(m[\"body\"], \"body\")\n        self.assertTrue(mb1.has_listeners())\n\n        mb1.add_message(SidedMessage(side=\"side2\", phase=\"phase2\",\n                                     body=\"body2\", server_rx=0,\n                                     msg_id=\"msgid\"))\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"message\")\n        self.assertEqual(m[\"body\"], \"body2\")\n\n        c1.send(\"open\", mailbox=\"mb1\")\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"only one open per connection\")\n\n        # exercise the _stop() handler too, which is a nop\n        mb1.close(\"side2\", \"happy\", 1)\n        mb1.close(\"side\", \"happy\", 2)\n\n    @inlineCallbacks\n    def test_open_crowded(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        mbid = app.claim_nameplate(np1, \"side1\", 0)\n        app.claim_nameplate(np1, \"side2\", 0)\n\n        # the third open will signal crowding\n        c1.send(\"open\", mailbox=mbid)\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"crowded\")\n\n    @inlineCallbacks\n    def test_add(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n        mb1 = app.open_mailbox(\"mb1\", \"side2\", 0)\n        l1 = []; stop1 = []; stop1_f = lambda: stop1.append(True)\n        mb1.add_listener(\"handle1\", l1.append, stop1_f)\n\n        c1.send(\"add\") # didn't open first\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"must open mailbox before adding\")\n\n        c1.send(\"open\", mailbox=\"mb1\")\n\n        c1.send(\"add\", body=\"body\") # missing phase=\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"missing 'phase'\")\n\n        c1.send(\"add\", phase=\"phase\") # missing body=\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"missing 'body'\")\n\n        c1.send(\"add\", phase=\"phase\", body=\"body\")\n        m = yield c1.next_non_ack() # echoed back\n        self.assertEqual(m[\"type\"], \"message\")\n        self.assertEqual(m[\"body\"], \"body\")\n\n        self.assertEqual(len(l1), 1)\n        self.assertEqual(l1[0].body, \"body\")\n\n    @inlineCallbacks\n    def test_close(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        c1.send(\"close\", mood=\"mood\") # must open first\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"close without mailbox must follow open\")\n\n        c1.send(\"open\", mailbox=\"mb1\")\n        yield c1.sync()\n        mb1 = app._mailboxes[\"mb1\"]\n        self.assertTrue(mb1.has_listeners())\n\n        c1.send(\"close\", mood=\"mood\")\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"closed\")\n        self.assertFalse(mb1.has_listeners())\n\n        c1.send(\"close\", mood=\"mood\") # already closed\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\", m)\n        self.assertEqual(err[\"error\"], \"only one close per connection\")\n\n    @inlineCallbacks\n    def test_close_named(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n\n        c1.send(\"open\", mailbox=\"mb1\")\n        yield c1.sync()\n\n        c1.send(\"close\", mailbox=\"mb1\", mood=\"mood\")\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"closed\")\n\n    @inlineCallbacks\n    def test_close_named_ignored(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n\n        c1.send(\"close\", mailbox=\"mb1\", mood=\"mood\") # no open first, ignored\n        m = yield c1.next_non_ack()\n        self.assertEqual(m[\"type\"], \"closed\")\n\n    @inlineCallbacks\n    def test_close_named_mismatch(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n\n        c1.send(\"open\", mailbox=\"mb1\")\n        yield c1.sync()\n\n        c1.send(\"close\", mailbox=\"mb2\", mood=\"mood\")\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"open and close must use same mailbox\")\n\n    @inlineCallbacks\n    def test_close_crowded(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        mbid = app.claim_nameplate(np1, \"side1\", 0)\n        app.claim_nameplate(np1, \"side2\", 0)\n\n        # a close that allocates a third side will signal crowding\n        c1.send(\"close\", mailbox=mbid)\n        err = yield c1.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"crowded\")\n\n\n    @inlineCallbacks\n    def test_disconnect(self):\n        c1 = yield self.make_client()\n        yield c1.next_non_ack()\n        c1.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        c1.send(\"open\", mailbox=\"mb1\")\n        yield c1.sync()\n        mb1 = app._mailboxes[\"mb1\"]\n        self.assertTrue(mb1.has_listeners())\n\n        yield c1.close()\n        # wait for the server to notice the socket has closed\n        started = time.time()\n        while mb1.has_listeners() and (time.time()-started < 5.0):\n            d = defer.Deferred()\n            reactor.callLater(0.01, d.callback, None)\n            yield d\n        self.assertFalse(mb1.has_listeners())\n\n    @inlineCallbacks\n    def test_interrupted_client_nameplate(self):\n        # a client's interactions with the server might be split over\n        # multiple sequential WebSocket connections, e.g. when the server is\n        # bounced and the client reconnects, or vice versa\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        c.send(\"claim\", nameplate=np1)\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"claimed\")\n        mailbox_id = m[\"mailbox\"]\n        self.assertEqual(type(mailbox_id), str)\n        np_row, side_rows = self._nameplate(app, np1)\n        claims = [(row[\"side\"], row[\"claimed\"]) for row in side_rows]\n        self.assertEqual(claims, [(\"side\", True)])\n        c.close()\n        yield c.d\n\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        c.send(\"claim\", nameplate=np1) # idempotent\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"claimed\")\n        self.assertEqual(m[\"mailbox\"], mailbox_id) # mailbox id is stable\n        np_row, side_rows = self._nameplate(app, np1)\n        claims = [(row[\"side\"], row[\"claimed\"]) for row in side_rows]\n        self.assertEqual(claims, [(\"side\", True)])\n        c.close()\n        yield c.d\n\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        # we haven't done a claim with this particular connection, but we can\n        # still send a release as long as we include the nameplate\n        c.send(\"release\", nameplate=np1) # release-without-claim\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"released\")\n        np_row, side_rows = self._nameplate(app, np1)\n        self.assertEqual(np_row, None)\n        c.close()\n        yield c.d\n\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        # and the release is idempotent, when done on separate connections\n        c.send(\"release\", nameplate=np1)\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"released\")\n        np_row, side_rows = self._nameplate(app, np1)\n        self.assertEqual(np_row, None)\n        c.close()\n        yield c.d\n\n\n    @inlineCallbacks\n    def test_interrupted_client_nameplate_reclaimed(self):\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n\n        # a new claim on a previously-closed nameplate is forbidden. We make\n        # a new nameplate here and manually open a second claim on it, so the\n        # nameplate stays alive long enough for the code check to happen.\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        c.send(\"claim\", nameplate=np2)\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"claimed\")\n        app.claim_nameplate(np2, \"side2\", 0)\n        c.send(\"release\", nameplate=np2)\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"released\")\n        np_row, side_rows = self._nameplate(app, np2)\n        claims = sorted([(row[\"side\"], row[\"claimed\"]) for row in side_rows])\n        self.assertEqual(claims, [(\"side\", 0), (\"side2\", 1)])\n        c.close()\n        yield c.d\n\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        c.send(\"claim\", nameplate=np2) # new claim is forbidden\n        err = yield c.next_non_ack()\n        self.assertEqual(err[\"type\"], \"error\")\n        self.assertEqual(err[\"error\"], \"reclaimed\")\n\n        np_row, side_rows = self._nameplate(app, np2)\n        claims = sorted([(row[\"side\"], row[\"claimed\"]) for row in side_rows])\n        self.assertEqual(claims, [(\"side\", 0), (\"side2\", 1)])\n        c.close()\n        yield c.d\n\n    @inlineCallbacks\n    def test_interrupted_client_mailbox(self):\n        # a client's interactions with the server might be split over\n        # multiple sequential WebSocket connections, e.g. when the server is\n        # bounced and the client reconnects, or vice versa\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        app = self._server.get_app(\"appid\")\n        mb1 = app.open_mailbox(\"mb1\", \"side2\", 0)\n        mb1.add_message(SidedMessage(side=\"side2\", phase=\"phase\",\n                                     body=\"body\", server_rx=0,\n                                     msg_id=\"msgid\"))\n\n        c.send(\"open\", mailbox=\"mb1\")\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"message\")\n        self.assertEqual(m[\"body\"], \"body\")\n        self.assertTrue(mb1.has_listeners())\n        c.close()\n        yield c.d\n\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        # open should be idempotent\n        c.send(\"open\", mailbox=\"mb1\")\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"message\")\n        self.assertEqual(m[\"body\"], \"body\")\n        mb_row, side_rows = self._mailbox(app, \"mb1\")\n        openeds = [(row[\"side\"], row[\"opened\"]) for row in side_rows]\n        self.assertIn((\"side\", 1), openeds) # TODO: why 1, and not True?\n\n        # close on the same connection as open is ok\n        c.send(\"close\", mailbox=\"mb1\", mood=\"mood\")\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"closed\", m)\n        mb_row, side_rows = self._mailbox(app, \"mb1\")\n        openeds = [(row[\"side\"], row[\"opened\"]) for row in side_rows]\n        self.assertIn((\"side\", 0), openeds)\n        c.close()\n        yield c.d\n\n        # close (on a separate connection) is idempotent\n        c = yield self.make_client()\n        yield c.next_non_ack()\n        c.send(\"bind\", appid=\"appid\", side=\"side\")\n        c.send(\"close\", mailbox=\"mb1\", mood=\"mood\")\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"closed\", m)\n        mb_row, side_rows = self._mailbox(app, \"mb1\")\n        openeds = [(row[\"side\"], row[\"opened\"]) for row in side_rows]\n        self.assertIn((\"side\", 0), openeds)\n        c.close()\n        yield c.d\n\n\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_websocket.py",
    "content": "import json\nfrom twisted.trial import unittest\nfrom twisted.internet.defer import inlineCallbacks\nfrom twisted.internet.address import IPv4Address\nfrom ..server_websocket import WebSocketServerFactory\nfrom autobahn.twisted.testing import create_pumper, create_memory_agent, MemoryReactorClock\nfrom autobahn.twisted.websocket import WebSocketClientProtocol\n\n\nclass FakeServer:\n    \"\"\"\n    Fake enough of the internal 'Server' object to appease the\n    WebSocket server.\n\n    \"\"\"\n    def get_welcome(self):\n        return {\n            \"motd\": \"fake message of the day\"\n        }\n\n    def get_log_requests(self):\n        return False\n\n\nclass WebSocket(unittest.TestCase):\n    \"\"\"\n    Details of the server WebSocket protocol\n    \"\"\"\n\n    def setUp(self):\n        self.pumper = create_pumper()\n        self.reactor = MemoryReactorClock()\n        return self.pumper.start()\n\n    def tearDown(self):\n        return self.pumper.stop()\n\n    def create_server_protocol(self):\n        \"\"\"\n        Used by the Agent to create the in-memory transport server-side\n        WebSocket protocol (we actually create the 'real' protocol\n        objects since that is what we're testing here.)\n        \"\"\"\n        factory = WebSocketServerFactory(\n            \"ws://localhost:4000/v1\",\n            FakeServer(),\n        )\n        addr = IPv4Address(\"TCP\", \"localhost\", 4000)\n        return factory.buildProtocol(addr)\n\n    @inlineCallbacks\n    def test_server_version_string(self):\n        \"\"\"\n        Our server version string from Autobahn should make sense\n        \"\"\"\n        server_header = None\n        agent = create_memory_agent(\n            self.reactor,\n            self.pumper,\n            self.create_server_protocol\n        )\n\n        class FakeClient(WebSocketClientProtocol):\n            def onConnect(self, cr):\n                nonlocal server_header\n                server_header = cr.headers.get(\"server\")\n\n        proto = yield agent.open(\"ws://localhost:4000/v1\", dict(), FakeClient)\n        proto.sendClose()\n        yield proto.is_closed\n\n        assert \"Magic Wormhole\" in server_header, \"Incorrect Server: header sent\"\n\n    @inlineCallbacks\n    def test_reflected_address(self):\n        \"\"\"\n        The Welcome message should include our address information\n        \"\"\"\n        welcome = None\n        agent = create_memory_agent(\n            self.reactor,\n            self.pumper,\n            self.create_server_protocol\n        )\n\n        class FakeClient(WebSocketClientProtocol):\n            def onMessage(self, payload, isBinary):\n                js = json.loads(payload)\n                nonlocal welcome\n                if welcome is None:\n                    welcome = js.get(\"welcome\", None)\n                return super().onMessage(payload, isBinary)\n\n        proto = yield agent.open(\"ws://localhost:4000/v1\", dict(), FakeClient)\n        proto.sendClose()\n        yield proto.is_closed\n\n        assert welcome is not None, \"Failed to receive Welcome message\"\n        ya = welcome.get(\"your-address\", None)\n        assert ya, \"Expected 'your-address' in Welcome message\"\n        assert ya[\"port\"] == 31337\n        assert \"ipv4\" in ya or \"ipv6\" in ya, \"Expected either IPv4 or IPv6 address\"\n\n    @inlineCallbacks\n    def test_reflected_caddy(self):\n        \"\"\"\n        The Welcome message should include our address information\n        when sent via x-real-ip headers\n        \"\"\"\n        welcome = None\n\n        headers = {\n            \"x-real-ip\": \"127.1.2.3\",\n            \"x-real-port\": \"54321\",\n        }\n\n        agent = create_memory_agent(\n            self.reactor,\n            self.pumper,\n            self.create_server_protocol,\n        )\n\n        class FakeClient(WebSocketClientProtocol):\n            def onMessage(self, payload, isBinary):\n                js = json.loads(payload)\n                nonlocal welcome\n                if welcome is None:\n                    welcome = js.get(\"welcome\", None)\n                return super().onMessage(payload, isBinary)\n\n        proto = yield agent.open(\"ws://localhost:4000/v1\", {\"headers\": headers}, FakeClient)\n        proto.sendClose()\n        yield proto.is_closed\n\n        assert welcome is not None, \"Failed to receive Welcome message\"\n        self.assertEqual(\n            welcome[\"your-address\"],\n            {\n                \"ipv4\": \"127.1.2.3\",\n                \"port\": 54321,\n            }\n        )\n\n    @inlineCallbacks\n    def test_reflected_caddyv6(self):\n        \"\"\"\n        The Welcome message should include our address information\n        when sent via x-real-ip headers (IPv6 version)\n        \"\"\"\n        welcome = None\n\n        headers = {\n            \"x-real-ip\": \"::1\",\n            \"x-real-port\": \"54321\",\n        }\n\n        agent = create_memory_agent(\n            self.reactor,\n            self.pumper,\n            self.create_server_protocol,\n        )\n\n        class FakeClient(WebSocketClientProtocol):\n            def onMessage(self, payload, isBinary):\n                js = json.loads(payload)\n                nonlocal welcome\n                if welcome is None:\n                    welcome = js.get(\"welcome\", None)\n                return super().onMessage(payload, isBinary)\n\n        proto = yield agent.open(\"ws://localhost:4000/v1\", {\"headers\": headers}, FakeClient)\n        proto.sendClose()\n        yield proto.is_closed\n\n        assert welcome is not None, \"Failed to receive Welcome message\"\n        self.assertEqual(\n            welcome[\"your-address\"],\n            {\n                \"ipv6\": \"::1\",\n                \"port\": 54321,\n            }\n        )\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/test_ws_client.py",
    "content": "import json\nfrom twisted.trial import unittest\nfrom twisted.internet.defer import inlineCallbacks\nfrom .ws_client import WSClient\n\nclass WSClientSync(unittest.TestCase):\n    # make sure my 'sync' method actually works\n\n    @inlineCallbacks\n    def test_sync(self):\n        sent = []\n        c = WSClient()\n        def _send(mtype, **kwargs):\n            sent.append( (mtype, kwargs) )\n        c.send = _send\n        def add(mtype, **kwargs):\n            kwargs[\"type\"] = mtype\n            c.onMessage(json.dumps(kwargs).encode(\"utf-8\"), False)\n        # no queued messages\n        d = c.sync()\n        self.assertEqual(sent, [(\"ping\", {\"ping\": 0})])\n        self.assertNoResult(d)\n        add(\"pong\", pong=0)\n        yield d\n        self.assertEqual(c.events, [])\n\n        # one,two,ping,pong\n        add(\"one\")\n        add(\"two\", two=2)\n        d = c.sync()\n        add(\"pong\", pong=1)\n        yield d\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"one\")\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"two\")\n        self.assertEqual(c.events, [])\n\n        # one,ping,two,pong\n        add(\"one\")\n        d = c.sync()\n        add(\"two\", two=2)\n        add(\"pong\", pong=2)\n        yield d\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"one\")\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"two\")\n        self.assertEqual(c.events, [])\n\n        # ping,one,two,pong\n        d = c.sync()\n        add(\"one\")\n        add(\"two\", two=2)\n        add(\"pong\", pong=3)\n        yield d\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"one\")\n        m = yield c.next_non_ack()\n        self.assertEqual(m[\"type\"], \"two\")\n        self.assertEqual(c.events, [])\n\n"
  },
  {
    "path": "src/wormhole_mailbox_server/test/ws_client.py",
    "content": "import json, itertools\nfrom twisted.internet import defer\nfrom twisted.internet.defer import inlineCallbacks\nfrom autobahn.twisted import websocket\n\nclass WSClient(websocket.WebSocketClientProtocol):\n    def __init__(self):\n        websocket.WebSocketClientProtocol.__init__(self)\n        self.events = []\n        self.errors = []\n        self.d = None\n        self.ping_counter = itertools.count(0)\n    def onOpen(self):\n        self.factory.d.callback(self)\n    def onMessage(self, payload, isBinary):\n        assert not isBinary\n        event = json.loads(payload.decode(\"utf-8\"))\n        if event[\"type\"] == \"error\":\n            self.errors.append(event)\n        if self.d:\n            assert not self.events\n            d,self.d = self.d,None\n            d.callback(event)\n            return\n        self.events.append(event)\n\n    def close(self):\n        self.d = defer.Deferred()\n        self.transport.loseConnection()\n        return self.d\n    def onClose(self, wasClean, code, reason):\n        if self.d:\n            self.d.callback((wasClean, code, reason))\n\n    def next_event(self):\n        assert not self.d\n        if self.events:\n            event = self.events.pop(0)\n            return defer.succeed(event)\n        self.d = defer.Deferred()\n        return self.d\n\n    @inlineCallbacks\n    def next_non_ack(self):\n        while True:\n            m = yield self.next_event()\n            if isinstance(m, tuple):\n                print(\"unexpected onClose\", m)\n                raise AssertionError(\"unexpected onClose\")\n            if m[\"type\"] != \"ack\":\n                return m\n\n    def strip_acks(self):\n        self.events = [e for e in self.events if e[\"type\"] != \"ack\"]\n\n    def send(self, mtype, **kwargs):\n        kwargs[\"type\"] = mtype\n        payload = json.dumps(kwargs).encode(\"utf-8\")\n        self.sendMessage(payload, False)\n\n    def send_notype(self, **kwargs):\n        payload = json.dumps(kwargs).encode(\"utf-8\")\n        self.sendMessage(payload, False)\n\n    @inlineCallbacks\n    def sync(self):\n        ping = next(self.ping_counter)\n        self.send(\"ping\", ping=ping)\n        # queue all messages until the pong, then put them back\n        old_events = []\n        while True:\n            ev = yield self.next_event()\n            if ev[\"type\"] == \"pong\" and ev[\"pong\"] == ping:\n                self.events = old_events + self.events\n                return None\n            old_events.append(ev)\n\nclass WSFactory(websocket.WebSocketClientFactory):\n    protocol = WSClient\n"
  },
  {
    "path": "src/wormhole_mailbox_server/util.py",
    "content": "# No unicode_literals\nimport json, unicodedata\nfrom binascii import hexlify, unhexlify\n\ndef to_bytes(u):\n    return unicodedata.normalize(\"NFC\", u).encode(\"utf-8\")\ndef bytes_to_hexstr(b):\n    assert isinstance(b, bytes)\n    hexstr = hexlify(b).decode(\"ascii\")\n    assert isinstance(hexstr, str)\n    return hexstr\ndef hexstr_to_bytes(hexstr):\n    assert isinstance(hexstr, str)\n    b = unhexlify(hexstr.encode(\"ascii\"))\n    assert isinstance(b, bytes)\n    return b\ndef dict_to_bytes(d):\n    assert isinstance(d, dict)\n    b = json.dumps(d).encode(\"utf-8\")\n    assert isinstance(b, bytes)\n    return b\ndef bytes_to_dict(b):\n    assert isinstance(b, bytes)\n    d = json.loads(b.decode(\"utf-8\"))\n    assert isinstance(d, dict)\n    return d\n"
  },
  {
    "path": "src/wormhole_mailbox_server/web.py",
    "content": "from twisted.web import server, static\nfrom twisted.web.resource import Resource\nfrom .server_websocket import WebSocketServerFactory\nfrom autobahn.twisted.resource import WebSocketResource\n\nclass Root(Resource):\n    # child_FOO is a nevow thing, not a twisted.web.resource thing\n    def __init__(self):\n        Resource.__init__(self)\n        self.putChild(b\"\", static.Data(b\"Wormhole Relay\\n\", \"text/plain\"))\n\nclass PrivacyEnhancedSite(server.Site):\n    logRequests = True\n    def log(self, request):\n        if self.logRequests:\n            return server.Site.log(self, request)\n\n\ndef make_web_server(server, log_requests, websocket_protocol_options=()):\n    root = Root()\n    wsrf = WebSocketServerFactory(None, server)\n    wsrf.setProtocolOptions(**dict(websocket_protocol_options))\n    root.putChild(b\"v1\", WebSocketResource(wsrf))\n\n    site = PrivacyEnhancedSite(root)\n    site.logRequests = log_requests\n\n    return site\n\n"
  },
  {
    "path": "tox.ini",
    "content": "# Tox (http://tox.testrun.org/) is a tool for running tests\n# in multiple virtualenvs. This configuration file will run the\n# test suite on all supported python versions. To use it, \"pip install tox\"\n# and then run \"tox\" from this directory.\n\n[tox]\nenvlist = {py310,py311,py312,py313,py314,pypy}\nskip_missing_interpreters = True\nminversion = 2.4.0\n\n[testenv]\nusedevelop = True\nextras = dev\ndeps =\n    pyflakes >= 1.2.3\nsetenv =\n    # set COLUMNS to standardize string output\n    COLUMNS=80\ncommands =\n    pyflakes setup.py src\n    python -m twisted.trial {posargs:wormhole_mailbox_server}\n\n\n# on windows, trial is installed as venv/bin/trial.py, not .exe, but (at\n# least appveyor) adds .PY to $PATHEXT. So \"trial wormhole\" might work on\n# windows, and certainly does on unix. But to get \"coverage run\" to work, we\n# need a script name (since \"python -m twisted.scripts.trial\" doesn't have a\n# 'if __name__ == \"__main__\": run()' -style clause), and the script name will\n# vary on the platform. So we added a small class (wormhole.test.run_trial)\n# that does the right import for us.\n\n[testenv:coverage]\ndeps =\n    pyflakes >= 1.2.3\n    coverage\ncommands =\n    pyflakes setup.py src\n    coverage run --branch -m twisted.trial {posargs:wormhole_mailbox_server}\n    coverage xml\n\n[testenv:flake8]\ndeps =\n     flake8\ncommands =\n     flake8 *.py src --count --select=E901,E999,F821,F822,F823 --statistics\n"
  },
  {
    "path": "update-version.py",
    "content": "#\n# this updates the (tagged) version of the software\n#\n# it will only update the \"minor\" version (e.g. 0.12.* -> 0.13.0)\n#\n# Any \"options\" are hard-coded in here (e.g. the GnuPG key to use)\n#\n\nimport sys\nimport time\nfrom datetime import datetime\n\nfrom dulwich.repo import Repo\nfrom dulwich.porcelain import (\n    tag_list,\n    tag_create,\n    status,\n)\n\nfrom twisted.internet.task import (\n    react,\n)\nfrom twisted.internet.defer import (\n    ensureDeferred,\n)\n\nauthor = \"meejah <meejah@meejah.ca>\"\n\n\ndef existing_tags(git):\n    versions = [\n        tuple(map(int, v.decode(\"utf8\").split(\".\")))\n        for v in tag_list(git)\n    ]\n    return versions\n\n\ndef create_new_version(git, only_patch):\n    versions = existing_tags(git)\n    major, minor, patch = sorted(versions)[-1]\n    if only_patch:\n        next_version = f\"{major}.{minor}.{patch + 1}\"\n    else:\n        next_version = f\"{major}.{minor + 1}.{0}\"\n    return next_version\n\n\nasync def main(reactor):\n    git = Repo(\".\")\n\n    # including untracked files can be very slow (if there are lots,\n    # like in virtualenvs) and we don't care anyway\n    st = status(git, untracked_files=\"no\")\n    if any(st.staged.values()) or st.unstaged:\n        print(\"unclean checkout; aborting\")\n        raise SystemExit(1)\n\n    for arg in sys.argv[1:]:\n        if arg not in (\"--no-tag\", \"--patch\"):\n            print(f\"unknown arg: {arg}\")\n            raise SystemExit(2)\n\n    v = create_new_version(git, \"--patch\" in sys.argv)\n    if \"--no-tag\" in sys.argv:\n        print(v)\n        return\n\n    print(\"Latest version: {}.{}.{}\".format(*sorted(existing_tags(git))[-1]))\n    print(f\"New tag will be {v}\")\n\n    # the \"tag time\" is seconds from the epoch .. we quantize these to\n    # the start of the day in question, in UTC.\n    now = datetime.now()\n    s = now.utctimetuple()\n    ts = int(\n        time.mktime(\n            time.struct_time((\n                s.tm_year, s.tm_mon, s.tm_mday, 0, 0, 0, 0, s.tm_yday, 0\n            ))\n        )\n    )\n    tag_create(\n        repo=git,\n        tag=v.encode(\"utf8\"),\n        author=author.encode(\"utf8\"),\n        message=f\"release magic-wormhole-{v}\".encode(\"utf8\"),\n        annotated=True,\n        objectish=b\"HEAD\",\n        sign=author.encode(\"utf8\"),\n        tag_time=ts,\n        tag_timezone=0,\n    )\n\n    print(\"Tag created locally, it is not pushed\")\n    print(\"To push it run something like:\")\n    print(f\"   git push origin {v}\")\n\n\nif __name__ == \"__main__\":\n    react(lambda r: ensureDeferred(main(r)))\n"
  },
  {
    "path": "versioneer.py",
    "content": "# Version: 0.29\n\n\"\"\"The Versioneer - like a rocketeer, but for versions.\n\nThe Versioneer\n==============\n\n* like a rocketeer, but for versions!\n* https://github.com/python-versioneer/python-versioneer\n* Brian Warner\n* License: Public Domain (Unlicense)\n* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3\n* [![Latest Version][pypi-image]][pypi-url]\n* [![Build Status][travis-image]][travis-url]\n\nThis is a tool for managing a recorded version number in setuptools-based\npython projects. The goal is to remove the tedious and error-prone \"update\nthe embedded version string\" step from your release process. Making a new\nrelease should be as easy as recording a new tag in your version-control\nsystem, and maybe making new tarballs.\n\n\n## Quick Install\n\nVersioneer provides two installation modes. The \"classic\" vendored mode installs\na copy of versioneer into your repository. The experimental build-time dependency mode\nis intended to allow you to skip this step and simplify the process of upgrading.\n\n### Vendored mode\n\n* `pip install versioneer` to somewhere in your $PATH\n   * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is\n     available, so you can also use `conda install -c conda-forge versioneer`\n* add a `[tool.versioneer]` section to your `pyproject.toml` or a\n  `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md))\n   * Note that you will need to add `tomli; python_version < \"3.11\"` to your\n     build-time dependencies if you use `pyproject.toml`\n* run `versioneer install --vendor` in your source tree, commit the results\n* verify version information with `python setup.py version`\n\n### Build-time dependency mode\n\n* `pip install versioneer` to somewhere in your $PATH\n   * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is\n     available, so you can also use `conda install -c conda-forge versioneer`\n* add a `[tool.versioneer]` section to your `pyproject.toml` or a\n  `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md))\n* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`)\n  to the `requires` key of the `build-system` table in `pyproject.toml`:\n  ```toml\n  [build-system]\n  requires = [\"setuptools\", \"versioneer[toml]\"]\n  build-backend = \"setuptools.build_meta\"\n  ```\n* run `versioneer install --no-vendor` in your source tree, commit the results\n* verify version information with `python setup.py version`\n\n## Version Identifiers\n\nSource trees come from a variety of places:\n\n* a version-control system checkout (mostly used by developers)\n* a nightly tarball, produced by build automation\n* a snapshot tarball, produced by a web-based VCS browser, like github's\n  \"tarball from tag\" feature\n* a release tarball, produced by \"setup.py sdist\", distributed through PyPI\n\nWithin each source tree, the version identifier (either a string or a number,\nthis tool is format-agnostic) can come from a variety of places:\n\n* ask the VCS tool itself, e.g. \"git describe\" (for checkouts), which knows\n  about recent \"tags\" and an absolute revision-id\n* the name of the directory into which the tarball was unpacked\n* an expanded VCS keyword ($Id$, etc)\n* a `_version.py` created by some earlier build step\n\nFor released software, the version identifier is closely related to a VCS\ntag. Some projects use tag names that include more than just the version\nstring (e.g. \"myproject-1.2\" instead of just \"1.2\"), in which case the tool\nneeds to strip the tag prefix to extract the version identifier. For\nunreleased software (between tags), the version identifier should provide\nenough information to help developers recreate the same tree, while also\ngiving them an idea of roughly how old the tree is (after version 1.2, before\nversion 1.3). Many VCS systems can report a description that captures this,\nfor example `git describe --tags --dirty --always` reports things like\n\"0.7-1-g574ab98-dirty\" to indicate that the checkout is one revision past the\n0.7 tag, has a unique revision id of \"574ab98\", and is \"dirty\" (it has\nuncommitted changes).\n\nThe version identifier is used for multiple purposes:\n\n* to allow the module to self-identify its version: `myproject.__version__`\n* to choose a name and prefix for a 'setup.py sdist' tarball\n\n## Theory of Operation\n\nVersioneer works by adding a special `_version.py` file into your source\ntree, where your `__init__.py` can import it. This `_version.py` knows how to\ndynamically ask the VCS tool for version information at import time.\n\n`_version.py` also contains `$Revision$` markers, and the installation\nprocess marks `_version.py` to have this marker rewritten with a tag name\nduring the `git archive` command. As a result, generated tarballs will\ncontain enough information to get the proper version.\n\nTo allow `setup.py` to compute a version too, a `versioneer.py` is added to\nthe top level of your source tree, next to `setup.py` and the `setup.cfg`\nthat configures it. This overrides several distutils/setuptools commands to\ncompute the version when invoked, and changes `setup.py build` and `setup.py\nsdist` to replace `_version.py` with a small static file that contains just\nthe generated version data.\n\n## Installation\n\nSee [INSTALL.md](./INSTALL.md) for detailed installation instructions.\n\n## Version-String Flavors\n\nCode which uses Versioneer can learn about its version string at runtime by\nimporting `_version` from your main `__init__.py` file and running the\n`get_versions()` function. From the \"outside\" (e.g. in `setup.py`), you can\nimport the top-level `versioneer.py` and run `get_versions()`.\n\nBoth functions return a dictionary with different flavors of version\ninformation:\n\n* `['version']`: A condensed version string, rendered using the selected\n  style. This is the most commonly used value for the project's version\n  string. The default \"pep440\" style yields strings like `0.11`,\n  `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the \"Styles\" section\n  below for alternative styles.\n\n* `['full-revisionid']`: detailed revision identifier. For Git, this is the\n  full SHA1 commit id, e.g. \"1076c978a8d3cfc70f408fe5974aa6c092c949ac\".\n\n* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the\n  commit date in ISO 8601 format. This will be None if the date is not\n  available.\n\n* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that\n  this is only accurate if run in a VCS checkout, otherwise it is likely to\n  be False or None\n\n* `['error']`: if the version string could not be computed, this will be set\n  to a string describing the problem, otherwise it will be None. It may be\n  useful to throw an exception in setup.py if this is set, to avoid e.g.\n  creating tarballs with a version string of \"unknown\".\n\nSome variants are more useful than others. Including `full-revisionid` in a\nbug report should allow developers to reconstruct the exact code being tested\n(or indicate the presence of local changes that should be shared with the\ndevelopers). `version` is suitable for display in an \"about\" box or a CLI\n`--version` output: it can be easily compared against release notes and lists\nof bugs fixed in various releases.\n\nThe installer adds the following text to your `__init__.py` to place a basic\nversion in `YOURPROJECT.__version__`:\n\n    from ._version import get_versions\n    __version__ = get_versions()['version']\n    del get_versions\n\n## Styles\n\nThe setup.cfg `style=` configuration controls how the VCS information is\nrendered into a version string.\n\nThe default style, \"pep440\", produces a PEP440-compliant string, equal to the\nun-prefixed tag name for actual releases, and containing an additional \"local\nversion\" section with more detail for in-between builds. For Git, this is\nTAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags\n--dirty --always`. For example \"0.11+2.g1076c97.dirty\" indicates that the\ntree is like the \"1076c97\" commit but has uncommitted changes (\".dirty\"), and\nthat this commit is two revisions (\"+2\") beyond the \"0.11\" tag. For released\nsoftware (exactly equal to a known tag), the identifier will only contain the\nstripped tag, e.g. \"0.11\".\n\nOther styles are available. See [details.md](details.md) in the Versioneer\nsource tree for descriptions.\n\n## Debugging\n\nVersioneer tries to avoid fatal errors: if something goes wrong, it will tend\nto return a version of \"0+unknown\". To investigate the problem, run `setup.py\nversion`, which will run the version-lookup code in a verbose mode, and will\ndisplay the full contents of `get_versions()` (including the `error` string,\nwhich may help identify what went wrong).\n\n## Known Limitations\n\nSome situations are known to cause problems for Versioneer. This details the\nmost significant ones. More can be found on Github\n[issues page](https://github.com/python-versioneer/python-versioneer/issues).\n\n### Subprojects\n\nVersioneer has limited support for source trees in which `setup.py` is not in\nthe root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are\ntwo common reasons why `setup.py` might not be in the root:\n\n* Source trees which contain multiple subprojects, such as\n  [Buildbot](https://github.com/buildbot/buildbot), which contains both\n  \"master\" and \"slave\" subprojects, each with their own `setup.py`,\n  `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI\n  distributions (and upload multiple independently-installable tarballs).\n* Source trees whose main purpose is to contain a C library, but which also\n  provide bindings to Python (and perhaps other languages) in subdirectories.\n\nVersioneer will look for `.git` in parent directories, and most operations\nshould get the right version string. However `pip` and `setuptools` have bugs\nand implementation details which frequently cause `pip install .` from a\nsubproject directory to fail to find a correct version string (so it usually\ndefaults to `0+unknown`).\n\n`pip install --editable .` should work correctly. `setup.py install` might\nwork too.\n\nPip-8.1.1 is known to have this problem, but hopefully it will get fixed in\nsome later version.\n\n[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking\nthis issue. The discussion in\n[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the\nissue from the Versioneer side in more detail.\n[pip PR#3176](https://github.com/pypa/pip/pull/3176) and\n[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve\npip to let Versioneer work correctly.\n\nVersioneer-0.16 and earlier only looked for a `.git` directory next to the\n`setup.cfg`, so subprojects were completely unsupported with those releases.\n\n### Editable installs with setuptools <= 18.5\n\n`setup.py develop` and `pip install --editable .` allow you to install a\nproject into a virtualenv once, then continue editing the source code (and\ntest) without re-installing after every change.\n\n\"Entry-point scripts\" (`setup(entry_points={\"console_scripts\": ..})`) are a\nconvenient way to specify executable scripts that should be installed along\nwith the python package.\n\nThese both work as expected when using modern setuptools. When using\nsetuptools-18.5 or earlier, however, certain operations will cause\n`pkg_resources.DistributionNotFound` errors when running the entrypoint\nscript, which must be resolved by re-installing the package. This happens\nwhen the install happens with one version, then the egg_info data is\nregenerated while a different version is checked out. Many setup.py commands\ncause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into\na different virtualenv), so this can be surprising.\n\n[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes\nthis one, but upgrading to a newer version of setuptools should probably\nresolve it.\n\n\n## Updating Versioneer\n\nTo upgrade your project to a new release of Versioneer, do the following:\n\n* install the new Versioneer (`pip install -U versioneer` or equivalent)\n* edit `setup.cfg` and `pyproject.toml`, if necessary,\n  to include any new configuration settings indicated by the release notes.\n  See [UPGRADING](./UPGRADING.md) for details.\n* re-run `versioneer install --[no-]vendor` in your source tree, to replace\n  `SRC/_version.py`\n* commit any changed files\n\n## Future Directions\n\nThis tool is designed to make it easily extended to other version-control\nsystems: all VCS-specific components are in separate directories like\nsrc/git/ . The top-level `versioneer.py` script is assembled from these\ncomponents by running make-versioneer.py . In the future, make-versioneer.py\nwill take a VCS name as an argument, and will construct a version of\n`versioneer.py` that is specific to the given VCS. It might also take the\nconfiguration arguments that are currently provided manually during\ninstallation by editing setup.py . Alternatively, it might go the other\ndirection and include code from all supported VCS systems, reducing the\nnumber of intermediate scripts.\n\n## Similar projects\n\n* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time\n  dependency\n* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of\n  versioneer\n* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools\n  plugin\n\n## License\n\nTo make Versioneer easier to embed, all its code is dedicated to the public\ndomain. The `_version.py` that it creates is also in the public domain.\nSpecifically, both are released under the \"Unlicense\", as described in\nhttps://unlicense.org/.\n\n[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg\n[pypi-url]: https://pypi.python.org/pypi/versioneer/\n[travis-image]:\nhttps://img.shields.io/travis/com/python-versioneer/python-versioneer.svg\n[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer\n\n\"\"\"\n# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring\n# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements\n# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error\n# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with\n# pylint:disable=attribute-defined-outside-init,too-many-arguments\n\nimport configparser\nimport errno\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union\nfrom typing import NoReturn\nimport functools\n\nhave_tomllib = True\nif sys.version_info >= (3, 11):\n    import tomllib\nelse:\n    try:\n        import tomli as tomllib\n    except ImportError:\n        have_tomllib = False\n\n\nclass VersioneerConfig:\n    \"\"\"Container for Versioneer configuration parameters.\"\"\"\n\n    VCS: str\n    style: str\n    tag_prefix: str\n    versionfile_source: str\n    versionfile_build: Optional[str]\n    parentdir_prefix: Optional[str]\n    verbose: Optional[bool]\n\n\ndef get_root() -> str:\n    \"\"\"Get the project root directory.\n\n    We require that all commands are run from the project root, i.e. the\n    directory that contains setup.py, setup.cfg, and versioneer.py .\n    \"\"\"\n    root = os.path.realpath(os.path.abspath(os.getcwd()))\n    setup_py = os.path.join(root, \"setup.py\")\n    pyproject_toml = os.path.join(root, \"pyproject.toml\")\n    versioneer_py = os.path.join(root, \"versioneer.py\")\n    if not (\n        os.path.exists(setup_py)\n        or os.path.exists(pyproject_toml)\n        or os.path.exists(versioneer_py)\n    ):\n        # allow 'python path/to/setup.py COMMAND'\n        root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0])))\n        setup_py = os.path.join(root, \"setup.py\")\n        pyproject_toml = os.path.join(root, \"pyproject.toml\")\n        versioneer_py = os.path.join(root, \"versioneer.py\")\n    if not (\n        os.path.exists(setup_py)\n        or os.path.exists(pyproject_toml)\n        or os.path.exists(versioneer_py)\n    ):\n        err = (\"Versioneer was unable to run the project root directory. \"\n               \"Versioneer requires setup.py to be executed from \"\n               \"its immediate directory (like 'python setup.py COMMAND'), \"\n               \"or in a way that lets it use sys.argv[0] to find the root \"\n               \"(like 'python path/to/setup.py COMMAND').\")\n        raise VersioneerBadRootError(err)\n    try:\n        # Certain runtime workflows (setup.py install/develop in a setuptools\n        # tree) execute all dependencies in a single python process, so\n        # \"versioneer\" may be imported multiple times, and python's shared\n        # module-import table will cache the first one. So we can't use\n        # os.path.dirname(__file__), as that will find whichever\n        # versioneer.py was first imported, even in later projects.\n        my_path = os.path.realpath(os.path.abspath(__file__))\n        me_dir = os.path.normcase(os.path.splitext(my_path)[0])\n        vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0])\n        if me_dir != vsr_dir and \"VERSIONEER_PEP518\" not in globals():\n            print(\"Warning: build in %s is using versioneer.py from %s\"\n                  % (os.path.dirname(my_path), versioneer_py))\n    except NameError:\n        pass\n    return root\n\n\ndef get_config_from_root(root: str) -> VersioneerConfig:\n    \"\"\"Read the project setup.cfg file to determine Versioneer config.\"\"\"\n    # This might raise OSError (if setup.cfg is missing), or\n    # configparser.NoSectionError (if it lacks a [versioneer] section), or\n    # configparser.NoOptionError (if it lacks \"VCS=\"). See the docstring at\n    # the top of versioneer.py for instructions on writing your setup.cfg .\n    root_pth = Path(root)\n    pyproject_toml = root_pth / \"pyproject.toml\"\n    setup_cfg = root_pth / \"setup.cfg\"\n    section: Union[dict[str, Any], configparser.SectionProxy, None] = None\n    if pyproject_toml.exists() and have_tomllib:\n        try:\n            with open(pyproject_toml, 'rb') as fobj:\n                pp = tomllib.load(fobj)\n            section = pp['tool']['versioneer']\n        except (tomllib.TOMLDecodeError, KeyError) as e:\n            print(f\"Failed to load config from {pyproject_toml}: {e}\")\n            print(\"Try to load it from setup.cfg\")\n    if not section:\n        parser = configparser.ConfigParser()\n        with open(setup_cfg) as cfg_file:\n            parser.read_file(cfg_file)\n        parser.get(\"versioneer\", \"VCS\")  # raise error if missing\n\n        section = parser[\"versioneer\"]\n\n    # `cast`` really shouldn't be used, but its simplest for the\n    # common VersioneerConfig users at the moment. We verify against\n    # `None` values elsewhere where it matters\n\n    cfg = VersioneerConfig()\n    cfg.VCS = section['VCS']\n    cfg.style = section.get(\"style\", \"\")\n    cfg.versionfile_source = cast(str, section.get(\"versionfile_source\"))\n    cfg.versionfile_build = section.get(\"versionfile_build\")\n    cfg.tag_prefix = cast(str, section.get(\"tag_prefix\"))\n    if cfg.tag_prefix in (\"''\", '\"\"', None):\n        cfg.tag_prefix = \"\"\n    cfg.parentdir_prefix = section.get(\"parentdir_prefix\")\n    if isinstance(section, configparser.SectionProxy):\n        # Make sure configparser translates to bool\n        cfg.verbose = section.getboolean(\"verbose\")\n    else:\n        cfg.verbose = section.get(\"verbose\")\n\n    return cfg\n\n\nclass NotThisMethod(Exception):\n    \"\"\"Exception raised if a method is not valid for the current scenario.\"\"\"\n\n\n# these dictionaries contain VCS-specific tools\nLONG_VERSION_PY: dict[str, str] = {}\nHANDLERS: dict[str, dict[str, Callable]] = {}\n\n\ndef register_vcs_handler(vcs: str, method: str) -> Callable:  # decorator\n    \"\"\"Create decorator to mark a method as the handler of a VCS.\"\"\"\n    def decorate(f: Callable) -> Callable:\n        \"\"\"Store f in HANDLERS[vcs][method].\"\"\"\n        HANDLERS.setdefault(vcs, {})[method] = f\n        return f\n    return decorate\n\n\ndef run_command(\n    commands: list[str],\n    args: list[str],\n    cwd: Optional[str] = None,\n    verbose: bool = False,\n    hide_stderr: bool = False,\n    env: Optional[dict[str, str]] = None,\n) -> tuple[Optional[str], Optional[int]]:\n    \"\"\"Call the given command(s).\"\"\"\n    assert isinstance(commands, list)\n    process = None\n\n    popen_kwargs: dict[str, Any] = {}\n    if sys.platform == \"win32\":\n        # This hides the console window if pythonw.exe is used\n        startupinfo = subprocess.STARTUPINFO()\n        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW\n        popen_kwargs[\"startupinfo\"] = startupinfo\n\n    for command in commands:\n        try:\n            dispcmd = str([command] + args)\n            # remember shell=False, so use git.cmd on windows, not just git\n            process = subprocess.Popen([command] + args, cwd=cwd, env=env,\n                                       stdout=subprocess.PIPE,\n                                       stderr=(subprocess.PIPE if hide_stderr\n                                               else None), **popen_kwargs)\n            break\n        except OSError as e:\n            if e.errno == errno.ENOENT:\n                continue\n            if verbose:\n                print(\"unable to run %s\" % dispcmd)\n                print(e)\n            return None, None\n    else:\n        if verbose:\n            print(\"unable to find command, tried {}\".format(commands))\n        return None, None\n    stdout = process.communicate()[0].strip().decode()\n    if process.returncode != 0:\n        if verbose:\n            print(\"unable to run %s (error)\" % dispcmd)\n            print(\"stdout was %s\" % stdout)\n        return None, process.returncode\n    return stdout, process.returncode\n\n\nLONG_VERSION_PY['git'] = r'''\n# This file helps to compute a version number in source trees obtained from\n# git-archive tarball (such as those provided by githubs download-from-tag\n# feature). Distribution tarballs (built by setup.py sdist) and build\n# directories (produced by setup.py build) will contain a much shorter file\n# that just contains the computed version number.\n\n# This file is released into the public domain.\n# Generated by versioneer-0.29\n# https://github.com/python-versioneer/python-versioneer\n\n\"\"\"Git implementation of _version.py.\"\"\"\n\nimport errno\nimport os\nimport re\nimport subprocess\nimport sys\nfrom typing import Any, Callable, Dict, List, Optional, Tuple\nimport functools\n\n\ndef get_keywords() -> Dict[str, str]:\n    \"\"\"Get the keywords needed to look up the version information.\"\"\"\n    # these strings will be replaced by git during git-archive.\n    # setup.py/versioneer.py will grep for the variable names, so they must\n    # each be defined on a line of their own. _version.py will just call\n    # get_keywords().\n    git_refnames = \"%(DOLLAR)sFormat:%%d%(DOLLAR)s\"\n    git_full = \"%(DOLLAR)sFormat:%%H%(DOLLAR)s\"\n    git_date = \"%(DOLLAR)sFormat:%%ci%(DOLLAR)s\"\n    keywords = {\"refnames\": git_refnames, \"full\": git_full, \"date\": git_date}\n    return keywords\n\n\nclass VersioneerConfig:\n    \"\"\"Container for Versioneer configuration parameters.\"\"\"\n\n    VCS: str\n    style: str\n    tag_prefix: str\n    parentdir_prefix: str\n    versionfile_source: str\n    verbose: bool\n\n\ndef get_config() -> VersioneerConfig:\n    \"\"\"Create, populate and return the VersioneerConfig() object.\"\"\"\n    # these strings are filled in when 'setup.py versioneer' creates\n    # _version.py\n    cfg = VersioneerConfig()\n    cfg.VCS = \"git\"\n    cfg.style = \"%(STYLE)s\"\n    cfg.tag_prefix = \"%(TAG_PREFIX)s\"\n    cfg.parentdir_prefix = \"%(PARENTDIR_PREFIX)s\"\n    cfg.versionfile_source = \"%(VERSIONFILE_SOURCE)s\"\n    cfg.verbose = False\n    return cfg\n\n\nclass NotThisMethod(Exception):\n    \"\"\"Exception raised if a method is not valid for the current scenario.\"\"\"\n\n\nLONG_VERSION_PY: Dict[str, str] = {}\nHANDLERS: Dict[str, Dict[str, Callable]] = {}\n\n\ndef register_vcs_handler(vcs: str, method: str) -> Callable:  # decorator\n    \"\"\"Create decorator to mark a method as the handler of a VCS.\"\"\"\n    def decorate(f: Callable) -> Callable:\n        \"\"\"Store f in HANDLERS[vcs][method].\"\"\"\n        if vcs not in HANDLERS:\n            HANDLERS[vcs] = {}\n        HANDLERS[vcs][method] = f\n        return f\n    return decorate\n\n\ndef run_command(\n    commands: List[str],\n    args: List[str],\n    cwd: Optional[str] = None,\n    verbose: bool = False,\n    hide_stderr: bool = False,\n    env: Optional[Dict[str, str]] = None,\n) -> Tuple[Optional[str], Optional[int]]:\n    \"\"\"Call the given command(s).\"\"\"\n    assert isinstance(commands, list)\n    process = None\n\n    popen_kwargs: Dict[str, Any] = {}\n    if sys.platform == \"win32\":\n        # This hides the console window if pythonw.exe is used\n        startupinfo = subprocess.STARTUPINFO()\n        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW\n        popen_kwargs[\"startupinfo\"] = startupinfo\n\n    for command in commands:\n        try:\n            dispcmd = str([command] + args)\n            # remember shell=False, so use git.cmd on windows, not just git\n            process = subprocess.Popen([command] + args, cwd=cwd, env=env,\n                                       stdout=subprocess.PIPE,\n                                       stderr=(subprocess.PIPE if hide_stderr\n                                               else None), **popen_kwargs)\n            break\n        except OSError as e:\n            if e.errno == errno.ENOENT:\n                continue\n            if verbose:\n                print(\"unable to run %%s\" %% dispcmd)\n                print(e)\n            return None, None\n    else:\n        if verbose:\n            print(\"unable to find command, tried %%s\" %% (commands,))\n        return None, None\n    stdout = process.communicate()[0].strip().decode()\n    if process.returncode != 0:\n        if verbose:\n            print(\"unable to run %%s (error)\" %% dispcmd)\n            print(\"stdout was %%s\" %% stdout)\n        return None, process.returncode\n    return stdout, process.returncode\n\n\ndef versions_from_parentdir(\n    parentdir_prefix: str,\n    root: str,\n    verbose: bool,\n) -> Dict[str, Any]:\n    \"\"\"Try to determine the version from the parent directory name.\n\n    Source tarballs conventionally unpack into a directory that includes both\n    the project name and a version string. We will also support searching up\n    two directory levels for an appropriately named parent directory\n    \"\"\"\n    rootdirs = []\n\n    for _ in range(3):\n        dirname = os.path.basename(root)\n        if dirname.startswith(parentdir_prefix):\n            return {\"version\": dirname[len(parentdir_prefix):],\n                    \"full-revisionid\": None,\n                    \"dirty\": False, \"error\": None, \"date\": None}\n        rootdirs.append(root)\n        root = os.path.dirname(root)  # up a level\n\n    if verbose:\n        print(\"Tried directories %%s but none started with prefix %%s\" %%\n              (str(rootdirs), parentdir_prefix))\n    raise NotThisMethod(\"rootdir doesn't start with parentdir_prefix\")\n\n\n@register_vcs_handler(\"git\", \"get_keywords\")\ndef git_get_keywords(versionfile_abs: str) -> Dict[str, str]:\n    \"\"\"Extract version information from the given file.\"\"\"\n    # the code embedded in _version.py can just fetch the value of these\n    # keywords. When used from setup.py, we don't want to import _version.py,\n    # so we do it with a regexp instead. This function is not used from\n    # _version.py.\n    keywords: Dict[str, str] = {}\n    try:\n        with open(versionfile_abs, \"r\") as fobj:\n            for line in fobj:\n                if line.strip().startswith(\"git_refnames =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"refnames\"] = mo.group(1)\n                if line.strip().startswith(\"git_full =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"full\"] = mo.group(1)\n                if line.strip().startswith(\"git_date =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"date\"] = mo.group(1)\n    except OSError:\n        pass\n    return keywords\n\n\n@register_vcs_handler(\"git\", \"keywords\")\ndef git_versions_from_keywords(\n    keywords: Dict[str, str],\n    tag_prefix: str,\n    verbose: bool,\n) -> Dict[str, Any]:\n    \"\"\"Get version information from git keywords.\"\"\"\n    if \"refnames\" not in keywords:\n        raise NotThisMethod(\"Short version file found\")\n    date = keywords.get(\"date\")\n    if date is not None:\n        # Use only the last line.  Previous lines may contain GPG signature\n        # information.\n        date = date.splitlines()[-1]\n\n        # git-2.2.0 added \"%%cI\", which expands to an ISO-8601 -compliant\n        # datestamp. However we prefer \"%%ci\" (which expands to an \"ISO-8601\n        # -like\" string, which we must then edit to make compliant), because\n        # it's been around since git-1.5.3, and it's too difficult to\n        # discover which version we're using, or to work around using an\n        # older one.\n        date = date.strip().replace(\" \", \"T\", 1).replace(\" \", \"\", 1)\n    refnames = keywords[\"refnames\"].strip()\n    if refnames.startswith(\"$Format\"):\n        if verbose:\n            print(\"keywords are unexpanded, not using\")\n        raise NotThisMethod(\"unexpanded keywords, not a git-archive tarball\")\n    refs = {r.strip() for r in refnames.strip(\"()\").split(\",\")}\n    # starting in git-1.8.3, tags are listed as \"tag: foo-1.0\" instead of\n    # just \"foo-1.0\". If we see a \"tag: \" prefix, prefer those.\n    TAG = \"tag: \"\n    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}\n    if not tags:\n        # Either we're using git < 1.8.3, or there really are no tags. We use\n        # a heuristic: assume all version tags have a digit. The old git %%d\n        # expansion behaves like git log --decorate=short and strips out the\n        # refs/heads/ and refs/tags/ prefixes that would let us distinguish\n        # between branches and tags. By ignoring refnames without digits, we\n        # filter out many common branch names like \"release\" and\n        # \"stabilization\", as well as \"HEAD\" and \"master\".\n        tags = {r for r in refs if re.search(r'\\d', r)}\n        if verbose:\n            print(\"discarding '%%s', no digits\" %% \",\".join(refs - tags))\n    if verbose:\n        print(\"likely tags: %%s\" %% \",\".join(sorted(tags)))\n    for ref in sorted(tags):\n        # sorting will prefer e.g. \"2.0\" over \"2.0rc1\"\n        if ref.startswith(tag_prefix):\n            r = ref[len(tag_prefix):]\n            # Filter out refs that exactly match prefix or that don't start\n            # with a number once the prefix is stripped (mostly a concern\n            # when prefix is '')\n            if not re.match(r'\\d', r):\n                continue\n            if verbose:\n                print(\"picking %%s\" %% r)\n            return {\"version\": r,\n                    \"full-revisionid\": keywords[\"full\"].strip(),\n                    \"dirty\": False, \"error\": None,\n                    \"date\": date}\n    # no suitable tags, so version is \"0+unknown\", but full hex is still there\n    if verbose:\n        print(\"no suitable tags, using unknown + full revision id\")\n    return {\"version\": \"0+unknown\",\n            \"full-revisionid\": keywords[\"full\"].strip(),\n            \"dirty\": False, \"error\": \"no suitable tags\", \"date\": None}\n\n\n@register_vcs_handler(\"git\", \"pieces_from_vcs\")\ndef git_pieces_from_vcs(\n    tag_prefix: str,\n    root: str,\n    verbose: bool,\n    runner: Callable = run_command\n) -> Dict[str, Any]:\n    \"\"\"Get version from 'git describe' in the root of the source tree.\n\n    This only gets called if the git-archive 'subst' keywords were *not*\n    expanded, and _version.py hasn't already been rewritten with a short\n    version string, meaning we're inside a checked out source tree.\n    \"\"\"\n    GITS = [\"git\"]\n    if sys.platform == \"win32\":\n        GITS = [\"git.cmd\", \"git.exe\"]\n\n    # GIT_DIR can interfere with correct operation of Versioneer.\n    # It may be intended to be passed to the Versioneer-versioned project,\n    # but that should not change where we get our version from.\n    env = os.environ.copy()\n    env.pop(\"GIT_DIR\", None)\n    runner = functools.partial(runner, env=env)\n\n    _, rc = runner(GITS, [\"rev-parse\", \"--git-dir\"], cwd=root,\n                   hide_stderr=not verbose)\n    if rc != 0:\n        if verbose:\n            print(\"Directory %%s not under git control\" %% root)\n        raise NotThisMethod(\"'git rev-parse --git-dir' returned error\")\n\n    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]\n    # if there isn't one, this yields HEX[-dirty] (no NUM)\n    describe_out, rc = runner(GITS, [\n        \"describe\", \"--tags\", \"--dirty\", \"--always\", \"--long\",\n        \"--match\", f\"{tag_prefix}[[:digit:]]*\"\n    ], cwd=root)\n    # --long was added in git-1.5.5\n    if describe_out is None:\n        raise NotThisMethod(\"'git describe' failed\")\n    describe_out = describe_out.strip()\n    full_out, rc = runner(GITS, [\"rev-parse\", \"HEAD\"], cwd=root)\n    if full_out is None:\n        raise NotThisMethod(\"'git rev-parse' failed\")\n    full_out = full_out.strip()\n\n    pieces: Dict[str, Any] = {}\n    pieces[\"long\"] = full_out\n    pieces[\"short\"] = full_out[:7]  # maybe improved later\n    pieces[\"error\"] = None\n\n    branch_name, rc = runner(GITS, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n                             cwd=root)\n    # --abbrev-ref was added in git-1.6.3\n    if rc != 0 or branch_name is None:\n        raise NotThisMethod(\"'git rev-parse --abbrev-ref' returned error\")\n    branch_name = branch_name.strip()\n\n    if branch_name == \"HEAD\":\n        # If we aren't exactly on a branch, pick a branch which represents\n        # the current commit. If all else fails, we are on a branchless\n        # commit.\n        branches, rc = runner(GITS, [\"branch\", \"--contains\"], cwd=root)\n        # --contains was added in git-1.5.4\n        if rc != 0 or branches is None:\n            raise NotThisMethod(\"'git branch --contains' returned error\")\n        branches = branches.split(\"\\n\")\n\n        # Remove the first line if we're running detached\n        if \"(\" in branches[0]:\n            branches.pop(0)\n\n        # Strip off the leading \"* \" from the list of branches.\n        branches = [branch[2:] for branch in branches]\n        if \"master\" in branches:\n            branch_name = \"master\"\n        elif not branches:\n            branch_name = None\n        else:\n            # Pick the first branch that is returned. Good or bad.\n            branch_name = branches[0]\n\n    pieces[\"branch\"] = branch_name\n\n    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]\n    # TAG might have hyphens.\n    git_describe = describe_out\n\n    # look for -dirty suffix\n    dirty = git_describe.endswith(\"-dirty\")\n    pieces[\"dirty\"] = dirty\n    if dirty:\n        git_describe = git_describe[:git_describe.rindex(\"-dirty\")]\n\n    # now we have TAG-NUM-gHEX or HEX\n\n    if \"-\" in git_describe:\n        # TAG-NUM-gHEX\n        mo = re.search(r'^(.+)-(\\d+)-g([0-9a-f]+)$', git_describe)\n        if not mo:\n            # unparsable. Maybe git-describe is misbehaving?\n            pieces[\"error\"] = (\"unable to parse git-describe output: '%%s'\"\n                               %% describe_out)\n            return pieces\n\n        # tag\n        full_tag = mo.group(1)\n        if not full_tag.startswith(tag_prefix):\n            if verbose:\n                fmt = \"tag '%%s' doesn't start with prefix '%%s'\"\n                print(fmt %% (full_tag, tag_prefix))\n            pieces[\"error\"] = (\"tag '%%s' doesn't start with prefix '%%s'\"\n                               %% (full_tag, tag_prefix))\n            return pieces\n        pieces[\"closest-tag\"] = full_tag[len(tag_prefix):]\n\n        # distance: number of commits since tag\n        pieces[\"distance\"] = int(mo.group(2))\n\n        # commit: short hex revision ID\n        pieces[\"short\"] = mo.group(3)\n\n    else:\n        # HEX: no tags\n        pieces[\"closest-tag\"] = None\n        out, rc = runner(GITS, [\"rev-list\", \"HEAD\", \"--left-right\"], cwd=root)\n        pieces[\"distance\"] = len(out.split())  # total number of commits\n\n    # commit date: see ISO-8601 comment in git_versions_from_keywords()\n    date = runner(GITS, [\"show\", \"-s\", \"--format=%%ci\", \"HEAD\"], cwd=root)[0].strip()\n    # Use only the last line.  Previous lines may contain GPG signature\n    # information.\n    date = date.splitlines()[-1]\n    pieces[\"date\"] = date.strip().replace(\" \", \"T\", 1).replace(\" \", \"\", 1)\n\n    return pieces\n\n\ndef plus_or_dot(pieces: Dict[str, Any]) -> str:\n    \"\"\"Return a + if we don't already have one, else return a .\"\"\"\n    if \"+\" in pieces.get(\"closest-tag\", \"\"):\n        return \".\"\n    return \"+\"\n\n\ndef render_pep440(pieces: Dict[str, Any]) -> str:\n    \"\"\"Build up version string, with post-release \"local version identifier\".\n\n    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you\n    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty\n\n    Exceptions:\n    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += plus_or_dot(pieces)\n            rendered += \"%%d.g%%s\" %% (pieces[\"distance\"], pieces[\"short\"])\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0+untagged.%%d.g%%s\" %% (pieces[\"distance\"],\n                                          pieces[\"short\"])\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef render_pep440_branch(pieces: Dict[str, Any]) -> str:\n    \"\"\"TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .\n\n    The \".dev0\" means not master branch. Note that .dev0 sorts backwards\n    (a feature branch will appear \"older\" than the master branch).\n\n    Exceptions:\n    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            if pieces[\"branch\"] != \"master\":\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += \"%%d.g%%s\" %% (pieces[\"distance\"], pieces[\"short\"])\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0\"\n        if pieces[\"branch\"] != \"master\":\n            rendered += \".dev0\"\n        rendered += \"+untagged.%%d.g%%s\" %% (pieces[\"distance\"],\n                                          pieces[\"short\"])\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef pep440_split_post(ver: str) -> Tuple[str, Optional[int]]:\n    \"\"\"Split pep440 version string at the post-release segment.\n\n    Returns the release segments before the post-release and the\n    post-release version number (or -1 if no post-release segment is present).\n    \"\"\"\n    vc = str.split(ver, \".post\")\n    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None\n\n\ndef render_pep440_pre(pieces: Dict[str, Any]) -> str:\n    \"\"\"TAG[.postN.devDISTANCE] -- No -dirty.\n\n    Exceptions:\n    1: no tags. 0.post0.devDISTANCE\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        if pieces[\"distance\"]:\n            # update the post release segment\n            tag_version, post_version = pep440_split_post(pieces[\"closest-tag\"])\n            rendered = tag_version\n            if post_version is not None:\n                rendered += \".post%%d.dev%%d\" %% (post_version + 1, pieces[\"distance\"])\n            else:\n                rendered += \".post0.dev%%d\" %% (pieces[\"distance\"])\n        else:\n            # no commits, use the tag as the version\n            rendered = pieces[\"closest-tag\"]\n    else:\n        # exception #1\n        rendered = \"0.post0.dev%%d\" %% pieces[\"distance\"]\n    return rendered\n\n\ndef render_pep440_post(pieces: Dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]+gHEX] .\n\n    The \".dev0\" means dirty. Note that .dev0 sorts backwards\n    (a dirty tree will appear \"older\" than the corresponding clean one),\n    but you shouldn't be releasing software with -dirty anyways.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%%d\" %% pieces[\"distance\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += \"g%%s\" %% pieces[\"short\"]\n    else:\n        # exception #1\n        rendered = \"0.post%%d\" %% pieces[\"distance\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dev0\"\n        rendered += \"+g%%s\" %% pieces[\"short\"]\n    return rendered\n\n\ndef render_pep440_post_branch(pieces: Dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .\n\n    The \".dev0\" means not master branch.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%%d\" %% pieces[\"distance\"]\n            if pieces[\"branch\"] != \"master\":\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += \"g%%s\" %% pieces[\"short\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0.post%%d\" %% pieces[\"distance\"]\n        if pieces[\"branch\"] != \"master\":\n            rendered += \".dev0\"\n        rendered += \"+g%%s\" %% pieces[\"short\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef render_pep440_old(pieces: Dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]] .\n\n    The \".dev0\" means dirty.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%%d\" %% pieces[\"distance\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dev0\"\n    else:\n        # exception #1\n        rendered = \"0.post%%d\" %% pieces[\"distance\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dev0\"\n    return rendered\n\n\ndef render_git_describe(pieces: Dict[str, Any]) -> str:\n    \"\"\"TAG[-DISTANCE-gHEX][-dirty].\n\n    Like 'git describe --tags --dirty --always'.\n\n    Exceptions:\n    1: no tags. HEX[-dirty]  (note: no 'g' prefix)\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"]:\n            rendered += \"-%%d-g%%s\" %% (pieces[\"distance\"], pieces[\"short\"])\n    else:\n        # exception #1\n        rendered = pieces[\"short\"]\n    if pieces[\"dirty\"]:\n        rendered += \"-dirty\"\n    return rendered\n\n\ndef render_git_describe_long(pieces: Dict[str, Any]) -> str:\n    \"\"\"TAG-DISTANCE-gHEX[-dirty].\n\n    Like 'git describe --tags --dirty --always -long'.\n    The distance/hash is unconditional.\n\n    Exceptions:\n    1: no tags. HEX[-dirty]  (note: no 'g' prefix)\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        rendered += \"-%%d-g%%s\" %% (pieces[\"distance\"], pieces[\"short\"])\n    else:\n        # exception #1\n        rendered = pieces[\"short\"]\n    if pieces[\"dirty\"]:\n        rendered += \"-dirty\"\n    return rendered\n\n\ndef render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]:\n    \"\"\"Render the given version pieces into the requested style.\"\"\"\n    if pieces[\"error\"]:\n        return {\"version\": \"unknown\",\n                \"full-revisionid\": pieces.get(\"long\"),\n                \"dirty\": None,\n                \"error\": pieces[\"error\"],\n                \"date\": None}\n\n    if not style or style == \"default\":\n        style = \"pep440\"  # the default\n\n    if style == \"pep440\":\n        rendered = render_pep440(pieces)\n    elif style == \"pep440-branch\":\n        rendered = render_pep440_branch(pieces)\n    elif style == \"pep440-pre\":\n        rendered = render_pep440_pre(pieces)\n    elif style == \"pep440-post\":\n        rendered = render_pep440_post(pieces)\n    elif style == \"pep440-post-branch\":\n        rendered = render_pep440_post_branch(pieces)\n    elif style == \"pep440-old\":\n        rendered = render_pep440_old(pieces)\n    elif style == \"git-describe\":\n        rendered = render_git_describe(pieces)\n    elif style == \"git-describe-long\":\n        rendered = render_git_describe_long(pieces)\n    else:\n        raise ValueError(\"unknown style '%%s'\" %% style)\n\n    return {\"version\": rendered, \"full-revisionid\": pieces[\"long\"],\n            \"dirty\": pieces[\"dirty\"], \"error\": None,\n            \"date\": pieces.get(\"date\")}\n\n\ndef get_versions() -> Dict[str, Any]:\n    \"\"\"Get version information or return default if unable to do so.\"\"\"\n    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have\n    # __file__, we can work backwards from there to the root. Some\n    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which\n    # case we can only use expanded keywords.\n\n    cfg = get_config()\n    verbose = cfg.verbose\n\n    try:\n        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,\n                                          verbose)\n    except NotThisMethod:\n        pass\n\n    try:\n        root = os.path.realpath(__file__)\n        # versionfile_source is the relative path from the top of the source\n        # tree (where the .git directory might live) to this file. Invert\n        # this to find the root from __file__.\n        for _ in cfg.versionfile_source.split('/'):\n            root = os.path.dirname(root)\n    except NameError:\n        return {\"version\": \"0+unknown\", \"full-revisionid\": None,\n                \"dirty\": None,\n                \"error\": \"unable to find root of source tree\",\n                \"date\": None}\n\n    try:\n        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)\n        return render(pieces, cfg.style)\n    except NotThisMethod:\n        pass\n\n    try:\n        if cfg.parentdir_prefix:\n            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)\n    except NotThisMethod:\n        pass\n\n    return {\"version\": \"0+unknown\", \"full-revisionid\": None,\n            \"dirty\": None,\n            \"error\": \"unable to compute version\", \"date\": None}\n'''\n\n\n@register_vcs_handler(\"git\", \"get_keywords\")\ndef git_get_keywords(versionfile_abs: str) -> dict[str, str]:\n    \"\"\"Extract version information from the given file.\"\"\"\n    # the code embedded in _version.py can just fetch the value of these\n    # keywords. When used from setup.py, we don't want to import _version.py,\n    # so we do it with a regexp instead. This function is not used from\n    # _version.py.\n    keywords: dict[str, str] = {}\n    try:\n        with open(versionfile_abs) as fobj:\n            for line in fobj:\n                if line.strip().startswith(\"git_refnames =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"refnames\"] = mo.group(1)\n                if line.strip().startswith(\"git_full =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"full\"] = mo.group(1)\n                if line.strip().startswith(\"git_date =\"):\n                    mo = re.search(r'=\\s*\"(.*)\"', line)\n                    if mo:\n                        keywords[\"date\"] = mo.group(1)\n    except OSError:\n        pass\n    return keywords\n\n\n@register_vcs_handler(\"git\", \"keywords\")\ndef git_versions_from_keywords(\n    keywords: dict[str, str],\n    tag_prefix: str,\n    verbose: bool,\n) -> dict[str, Any]:\n    \"\"\"Get version information from git keywords.\"\"\"\n    if \"refnames\" not in keywords:\n        raise NotThisMethod(\"Short version file found\")\n    date = keywords.get(\"date\")\n    if date is not None:\n        # Use only the last line.  Previous lines may contain GPG signature\n        # information.\n        date = date.splitlines()[-1]\n\n        # git-2.2.0 added \"%cI\", which expands to an ISO-8601 -compliant\n        # datestamp. However we prefer \"%ci\" (which expands to an \"ISO-8601\n        # -like\" string, which we must then edit to make compliant), because\n        # it's been around since git-1.5.3, and it's too difficult to\n        # discover which version we're using, or to work around using an\n        # older one.\n        date = date.strip().replace(\" \", \"T\", 1).replace(\" \", \"\", 1)\n    refnames = keywords[\"refnames\"].strip()\n    if refnames.startswith(\"$Format\"):\n        if verbose:\n            print(\"keywords are unexpanded, not using\")\n        raise NotThisMethod(\"unexpanded keywords, not a git-archive tarball\")\n    refs = {r.strip() for r in refnames.strip(\"()\").split(\",\")}\n    # starting in git-1.8.3, tags are listed as \"tag: foo-1.0\" instead of\n    # just \"foo-1.0\". If we see a \"tag: \" prefix, prefer those.\n    TAG = \"tag: \"\n    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}\n    if not tags:\n        # Either we're using git < 1.8.3, or there really are no tags. We use\n        # a heuristic: assume all version tags have a digit. The old git %d\n        # expansion behaves like git log --decorate=short and strips out the\n        # refs/heads/ and refs/tags/ prefixes that would let us distinguish\n        # between branches and tags. By ignoring refnames without digits, we\n        # filter out many common branch names like \"release\" and\n        # \"stabilization\", as well as \"HEAD\" and \"master\".\n        tags = {r for r in refs if re.search(r'\\d', r)}\n        if verbose:\n            print(\"discarding '%s', no digits\" % \",\".join(refs - tags))\n    if verbose:\n        print(\"likely tags: %s\" % \",\".join(sorted(tags)))\n    for ref in sorted(tags):\n        # sorting will prefer e.g. \"2.0\" over \"2.0rc1\"\n        if ref.startswith(tag_prefix):\n            r = ref[len(tag_prefix):]\n            # Filter out refs that exactly match prefix or that don't start\n            # with a number once the prefix is stripped (mostly a concern\n            # when prefix is '')\n            if not re.match(r'\\d', r):\n                continue\n            if verbose:\n                print(\"picking %s\" % r)\n            return {\"version\": r,\n                    \"full-revisionid\": keywords[\"full\"].strip(),\n                    \"dirty\": False, \"error\": None,\n                    \"date\": date}\n    # no suitable tags, so version is \"0+unknown\", but full hex is still there\n    if verbose:\n        print(\"no suitable tags, using unknown + full revision id\")\n    return {\"version\": \"0+unknown\",\n            \"full-revisionid\": keywords[\"full\"].strip(),\n            \"dirty\": False, \"error\": \"no suitable tags\", \"date\": None}\n\n\n@register_vcs_handler(\"git\", \"pieces_from_vcs\")\ndef git_pieces_from_vcs(\n    tag_prefix: str,\n    root: str,\n    verbose: bool,\n    runner: Callable = run_command\n) -> dict[str, Any]:\n    \"\"\"Get version from 'git describe' in the root of the source tree.\n\n    This only gets called if the git-archive 'subst' keywords were *not*\n    expanded, and _version.py hasn't already been rewritten with a short\n    version string, meaning we're inside a checked out source tree.\n    \"\"\"\n    GITS = [\"git\"]\n    if sys.platform == \"win32\":\n        GITS = [\"git.cmd\", \"git.exe\"]\n\n    # GIT_DIR can interfere with correct operation of Versioneer.\n    # It may be intended to be passed to the Versioneer-versioned project,\n    # but that should not change where we get our version from.\n    env = os.environ.copy()\n    env.pop(\"GIT_DIR\", None)\n    runner = functools.partial(runner, env=env)\n\n    _, rc = runner(GITS, [\"rev-parse\", \"--git-dir\"], cwd=root,\n                   hide_stderr=not verbose)\n    if rc != 0:\n        if verbose:\n            print(\"Directory %s not under git control\" % root)\n        raise NotThisMethod(\"'git rev-parse --git-dir' returned error\")\n\n    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]\n    # if there isn't one, this yields HEX[-dirty] (no NUM)\n    describe_out, rc = runner(GITS, [\n        \"describe\", \"--tags\", \"--dirty\", \"--always\", \"--long\",\n        \"--match\", f\"{tag_prefix}[[:digit:]]*\"\n    ], cwd=root)\n    # --long was added in git-1.5.5\n    if describe_out is None:\n        raise NotThisMethod(\"'git describe' failed\")\n    describe_out = describe_out.strip()\n    full_out, rc = runner(GITS, [\"rev-parse\", \"HEAD\"], cwd=root)\n    if full_out is None:\n        raise NotThisMethod(\"'git rev-parse' failed\")\n    full_out = full_out.strip()\n\n    pieces: dict[str, Any] = {}\n    pieces[\"long\"] = full_out\n    pieces[\"short\"] = full_out[:7]  # maybe improved later\n    pieces[\"error\"] = None\n\n    branch_name, rc = runner(GITS, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n                             cwd=root)\n    # --abbrev-ref was added in git-1.6.3\n    if rc != 0 or branch_name is None:\n        raise NotThisMethod(\"'git rev-parse --abbrev-ref' returned error\")\n    branch_name = branch_name.strip()\n\n    if branch_name == \"HEAD\":\n        # If we aren't exactly on a branch, pick a branch which represents\n        # the current commit. If all else fails, we are on a branchless\n        # commit.\n        branches, rc = runner(GITS, [\"branch\", \"--contains\"], cwd=root)\n        # --contains was added in git-1.5.4\n        if rc != 0 or branches is None:\n            raise NotThisMethod(\"'git branch --contains' returned error\")\n        branches = branches.split(\"\\n\")\n\n        # Remove the first line if we're running detached\n        if \"(\" in branches[0]:\n            branches.pop(0)\n\n        # Strip off the leading \"* \" from the list of branches.\n        branches = [branch[2:] for branch in branches]\n        if \"master\" in branches:\n            branch_name = \"master\"\n        elif not branches:\n            branch_name = None\n        else:\n            # Pick the first branch that is returned. Good or bad.\n            branch_name = branches[0]\n\n    pieces[\"branch\"] = branch_name\n\n    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]\n    # TAG might have hyphens.\n    git_describe = describe_out\n\n    # look for -dirty suffix\n    dirty = git_describe.endswith(\"-dirty\")\n    pieces[\"dirty\"] = dirty\n    if dirty:\n        git_describe = git_describe[:git_describe.rindex(\"-dirty\")]\n\n    # now we have TAG-NUM-gHEX or HEX\n\n    if \"-\" in git_describe:\n        # TAG-NUM-gHEX\n        mo = re.search(r'^(.+)-(\\d+)-g([0-9a-f]+)$', git_describe)\n        if not mo:\n            # unparsable. Maybe git-describe is misbehaving?\n            pieces[\"error\"] = (\"unable to parse git-describe output: '%s'\"\n                               % describe_out)\n            return pieces\n\n        # tag\n        full_tag = mo.group(1)\n        if not full_tag.startswith(tag_prefix):\n            if verbose:\n                fmt = \"tag '%s' doesn't start with prefix '%s'\"\n                print(fmt % (full_tag, tag_prefix))\n            pieces[\"error\"] = (\"tag '%s' doesn't start with prefix '%s'\"\n                               % (full_tag, tag_prefix))\n            return pieces\n        pieces[\"closest-tag\"] = full_tag[len(tag_prefix):]\n\n        # distance: number of commits since tag\n        pieces[\"distance\"] = int(mo.group(2))\n\n        # commit: short hex revision ID\n        pieces[\"short\"] = mo.group(3)\n\n    else:\n        # HEX: no tags\n        pieces[\"closest-tag\"] = None\n        out, rc = runner(GITS, [\"rev-list\", \"HEAD\", \"--left-right\"], cwd=root)\n        pieces[\"distance\"] = len(out.split())  # total number of commits\n\n    # commit date: see ISO-8601 comment in git_versions_from_keywords()\n    date = runner(GITS, [\"show\", \"-s\", \"--format=%ci\", \"HEAD\"], cwd=root)[0].strip()\n    # Use only the last line.  Previous lines may contain GPG signature\n    # information.\n    date = date.splitlines()[-1]\n    pieces[\"date\"] = date.strip().replace(\" \", \"T\", 1).replace(\" \", \"\", 1)\n\n    return pieces\n\n\ndef do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None:\n    \"\"\"Git-specific installation logic for Versioneer.\n\n    For Git, this means creating/changing .gitattributes to mark _version.py\n    for export-subst keyword substitution.\n    \"\"\"\n    GITS = [\"git\"]\n    if sys.platform == \"win32\":\n        GITS = [\"git.cmd\", \"git.exe\"]\n    files = [versionfile_source]\n    if ipy:\n        files.append(ipy)\n    if \"VERSIONEER_PEP518\" not in globals():\n        try:\n            my_path = __file__\n            if my_path.endswith((\".pyc\", \".pyo\")):\n                my_path = os.path.splitext(my_path)[0] + \".py\"\n            versioneer_file = os.path.relpath(my_path)\n        except NameError:\n            versioneer_file = \"versioneer.py\"\n        files.append(versioneer_file)\n    present = False\n    try:\n        with open(\".gitattributes\") as fobj:\n            for line in fobj:\n                if line.strip().startswith(versionfile_source):\n                    if \"export-subst\" in line.strip().split()[1:]:\n                        present = True\n                        break\n    except OSError:\n        pass\n    if not present:\n        with open(\".gitattributes\", \"a+\") as fobj:\n            fobj.write(f\"{versionfile_source} export-subst\\n\")\n        files.append(\".gitattributes\")\n    run_command(GITS, [\"add\", \"--\"] + files)\n\n\ndef versions_from_parentdir(\n    parentdir_prefix: str,\n    root: str,\n    verbose: bool,\n) -> dict[str, Any]:\n    \"\"\"Try to determine the version from the parent directory name.\n\n    Source tarballs conventionally unpack into a directory that includes both\n    the project name and a version string. We will also support searching up\n    two directory levels for an appropriately named parent directory\n    \"\"\"\n    rootdirs = []\n\n    for _ in range(3):\n        dirname = os.path.basename(root)\n        if dirname.startswith(parentdir_prefix):\n            return {\"version\": dirname[len(parentdir_prefix):],\n                    \"full-revisionid\": None,\n                    \"dirty\": False, \"error\": None, \"date\": None}\n        rootdirs.append(root)\n        root = os.path.dirname(root)  # up a level\n\n    if verbose:\n        print(\"Tried directories %s but none started with prefix %s\" %\n              (str(rootdirs), parentdir_prefix))\n    raise NotThisMethod(\"rootdir doesn't start with parentdir_prefix\")\n\n\nSHORT_VERSION_PY = \"\"\"\n# This file was generated by 'versioneer.py' (0.29) from\n# revision-control system data, or from the parent directory name of an\n# unpacked source archive. Distribution tarballs contain a pre-generated copy\n# of this file.\n\nimport json\n\nversion_json = '''\n%s\n'''  # END VERSION_JSON\n\n\ndef get_versions():\n    return json.loads(version_json)\n\"\"\"\n\n\ndef versions_from_file(filename: str) -> dict[str, Any]:\n    \"\"\"Try to determine the version from _version.py if present.\"\"\"\n    try:\n        with open(filename) as f:\n            contents = f.read()\n    except OSError:\n        raise NotThisMethod(\"unable to read _version.py\")\n    mo = re.search(r\"version_json = '''\\n(.*)'''  # END VERSION_JSON\",\n                   contents, re.M | re.S)\n    if not mo:\n        mo = re.search(r\"version_json = '''\\r\\n(.*)'''  # END VERSION_JSON\",\n                       contents, re.M | re.S)\n    if not mo:\n        raise NotThisMethod(\"no version_json in _version.py\")\n    return json.loads(mo.group(1))\n\n\ndef write_to_version_file(filename: str, versions: dict[str, Any]) -> None:\n    \"\"\"Write the given version number to the given _version.py file.\"\"\"\n    contents = json.dumps(versions, sort_keys=True,\n                          indent=1, separators=(\",\", \": \"))\n    with open(filename, \"w\") as f:\n        f.write(SHORT_VERSION_PY % contents)\n\n    print(\"set {} to '{}'\".format(filename, versions[\"version\"]))\n\n\ndef plus_or_dot(pieces: dict[str, Any]) -> str:\n    \"\"\"Return a + if we don't already have one, else return a .\"\"\"\n    if \"+\" in pieces.get(\"closest-tag\", \"\"):\n        return \".\"\n    return \"+\"\n\n\ndef render_pep440(pieces: dict[str, Any]) -> str:\n    \"\"\"Build up version string, with post-release \"local version identifier\".\n\n    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you\n    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty\n\n    Exceptions:\n    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += plus_or_dot(pieces)\n            rendered += \"%d.g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0+untagged.%d.g%s\" % (pieces[\"distance\"],\n                                          pieces[\"short\"])\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef render_pep440_branch(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .\n\n    The \".dev0\" means not master branch. Note that .dev0 sorts backwards\n    (a feature branch will appear \"older\" than the master branch).\n\n    Exceptions:\n    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            if pieces[\"branch\"] != \"master\":\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += \"%d.g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0\"\n        if pieces[\"branch\"] != \"master\":\n            rendered += \".dev0\"\n        rendered += \"+untagged.%d.g%s\" % (pieces[\"distance\"],\n                                          pieces[\"short\"])\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef pep440_split_post(ver: str) -> tuple[str, Optional[int]]:\n    \"\"\"Split pep440 version string at the post-release segment.\n\n    Returns the release segments before the post-release and the\n    post-release version number (or -1 if no post-release segment is present).\n    \"\"\"\n    vc = str.split(ver, \".post\")\n    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None\n\n\ndef render_pep440_pre(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[.postN.devDISTANCE] -- No -dirty.\n\n    Exceptions:\n    1: no tags. 0.post0.devDISTANCE\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        if pieces[\"distance\"]:\n            # update the post release segment\n            tag_version, post_version = pep440_split_post(pieces[\"closest-tag\"])\n            rendered = tag_version\n            if post_version is not None:\n                rendered += \".post%d.dev%d\" % (post_version + 1, pieces[\"distance\"])\n            else:\n                rendered += \".post0.dev%d\" % (pieces[\"distance\"])\n        else:\n            # no commits, use the tag as the version\n            rendered = pieces[\"closest-tag\"]\n    else:\n        # exception #1\n        rendered = \"0.post0.dev%d\" % pieces[\"distance\"]\n    return rendered\n\n\ndef render_pep440_post(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]+gHEX] .\n\n    The \".dev0\" means dirty. Note that .dev0 sorts backwards\n    (a dirty tree will appear \"older\" than the corresponding clean one),\n    but you shouldn't be releasing software with -dirty anyways.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%d\" % pieces[\"distance\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += \"g%s\" % pieces[\"short\"]\n    else:\n        # exception #1\n        rendered = \"0.post%d\" % pieces[\"distance\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dev0\"\n        rendered += \"+g%s\" % pieces[\"short\"]\n    return rendered\n\n\ndef render_pep440_post_branch(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .\n\n    The \".dev0\" means not master branch.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%d\" % pieces[\"distance\"]\n            if pieces[\"branch\"] != \"master\":\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += \"g%s\" % pieces[\"short\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0.post%d\" % pieces[\"distance\"]\n        if pieces[\"branch\"] != \"master\":\n            rendered += \".dev0\"\n        rendered += \"+g%s\" % pieces[\"short\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef render_pep440_old(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[.postDISTANCE[.dev0]] .\n\n    The \".dev0\" means dirty.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%d\" % pieces[\"distance\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dev0\"\n    else:\n        # exception #1\n        rendered = \"0.post%d\" % pieces[\"distance\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dev0\"\n    return rendered\n\n\ndef render_git_describe(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG[-DISTANCE-gHEX][-dirty].\n\n    Like 'git describe --tags --dirty --always'.\n\n    Exceptions:\n    1: no tags. HEX[-dirty]  (note: no 'g' prefix)\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"]:\n            rendered += \"-%d-g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n    else:\n        # exception #1\n        rendered = pieces[\"short\"]\n    if pieces[\"dirty\"]:\n        rendered += \"-dirty\"\n    return rendered\n\n\ndef render_git_describe_long(pieces: dict[str, Any]) -> str:\n    \"\"\"TAG-DISTANCE-gHEX[-dirty].\n\n    Like 'git describe --tags --dirty --always -long'.\n    The distance/hash is unconditional.\n\n    Exceptions:\n    1: no tags. HEX[-dirty]  (note: no 'g' prefix)\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        rendered += \"-%d-g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n    else:\n        # exception #1\n        rendered = pieces[\"short\"]\n    if pieces[\"dirty\"]:\n        rendered += \"-dirty\"\n    return rendered\n\n\ndef render(pieces: dict[str, Any], style: str) -> dict[str, Any]:\n    \"\"\"Render the given version pieces into the requested style.\"\"\"\n    if pieces[\"error\"]:\n        return {\"version\": \"unknown\",\n                \"full-revisionid\": pieces.get(\"long\"),\n                \"dirty\": None,\n                \"error\": pieces[\"error\"],\n                \"date\": None}\n\n    if not style or style == \"default\":\n        style = \"pep440\"  # the default\n\n    if style == \"pep440\":\n        rendered = render_pep440(pieces)\n    elif style == \"pep440-branch\":\n        rendered = render_pep440_branch(pieces)\n    elif style == \"pep440-pre\":\n        rendered = render_pep440_pre(pieces)\n    elif style == \"pep440-post\":\n        rendered = render_pep440_post(pieces)\n    elif style == \"pep440-post-branch\":\n        rendered = render_pep440_post_branch(pieces)\n    elif style == \"pep440-old\":\n        rendered = render_pep440_old(pieces)\n    elif style == \"git-describe\":\n        rendered = render_git_describe(pieces)\n    elif style == \"git-describe-long\":\n        rendered = render_git_describe_long(pieces)\n    else:\n        raise ValueError(\"unknown style '%s'\" % style)\n\n    return {\"version\": rendered, \"full-revisionid\": pieces[\"long\"],\n            \"dirty\": pieces[\"dirty\"], \"error\": None,\n            \"date\": pieces.get(\"date\")}\n\n\nclass VersioneerBadRootError(Exception):\n    \"\"\"The project root directory is unknown or missing key files.\"\"\"\n\n\ndef get_versions(verbose: bool = False) -> dict[str, Any]:\n    \"\"\"Get the project version from whatever source is available.\n\n    Returns dict with two keys: 'version' and 'full'.\n    \"\"\"\n    if \"versioneer\" in sys.modules:\n        # see the discussion in cmdclass.py:get_cmdclass()\n        del sys.modules[\"versioneer\"]\n\n    root = get_root()\n    cfg = get_config_from_root(root)\n\n    assert cfg.VCS is not None, \"please set [versioneer]VCS= in setup.cfg\"\n    handlers = HANDLERS.get(cfg.VCS)\n    assert handlers, \"unrecognized VCS '%s'\" % cfg.VCS\n    verbose = verbose or bool(cfg.verbose)  # `bool()` used to avoid `None`\n    assert cfg.versionfile_source is not None, \\\n        \"please set versioneer.versionfile_source\"\n    assert cfg.tag_prefix is not None, \"please set versioneer.tag_prefix\"\n\n    versionfile_abs = os.path.join(root, cfg.versionfile_source)\n\n    # extract version from first of: _version.py, VCS command (e.g. 'git\n    # describe'), parentdir. This is meant to work for developers using a\n    # source checkout, for users of a tarball created by 'setup.py sdist',\n    # and for users of a tarball/zipball created by 'git archive' or github's\n    # download-from-tag feature or the equivalent in other VCSes.\n\n    get_keywords_f = handlers.get(\"get_keywords\")\n    from_keywords_f = handlers.get(\"keywords\")\n    if get_keywords_f and from_keywords_f:\n        try:\n            keywords = get_keywords_f(versionfile_abs)\n            ver = from_keywords_f(keywords, cfg.tag_prefix, verbose)\n            if verbose:\n                print(\"got version from expanded keyword %s\" % ver)\n            return ver\n        except NotThisMethod:\n            pass\n\n    try:\n        ver = versions_from_file(versionfile_abs)\n        if verbose:\n            print(\"got version from file {} {}\".format(versionfile_abs, ver))\n        return ver\n    except NotThisMethod:\n        pass\n\n    from_vcs_f = handlers.get(\"pieces_from_vcs\")\n    if from_vcs_f:\n        try:\n            pieces = from_vcs_f(cfg.tag_prefix, root, verbose)\n            ver = render(pieces, cfg.style)\n            if verbose:\n                print(\"got version from VCS %s\" % ver)\n            return ver\n        except NotThisMethod:\n            pass\n\n    try:\n        if cfg.parentdir_prefix:\n            ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose)\n            if verbose:\n                print(\"got version from parentdir %s\" % ver)\n            return ver\n    except NotThisMethod:\n        pass\n\n    if verbose:\n        print(\"unable to compute version\")\n\n    return {\"version\": \"0+unknown\", \"full-revisionid\": None,\n            \"dirty\": None, \"error\": \"unable to compute version\",\n            \"date\": None}\n\n\ndef get_version() -> str:\n    \"\"\"Get the short version string for this project.\"\"\"\n    return get_versions()[\"version\"]\n\n\ndef get_cmdclass(cmdclass: Optional[dict[str, Any]] = None):\n    \"\"\"Get the custom setuptools subclasses used by Versioneer.\n\n    If the package uses a different cmdclass (e.g. one from numpy), it\n    should be provide as an argument.\n    \"\"\"\n    if \"versioneer\" in sys.modules:\n        del sys.modules[\"versioneer\"]\n        # this fixes the \"python setup.py develop\" case (also 'install' and\n        # 'easy_install .'), in which subdependencies of the main project are\n        # built (using setup.py bdist_egg) in the same python process. Assume\n        # a main project A and a dependency B, which use different versions\n        # of Versioneer. A's setup.py imports A's Versioneer, leaving it in\n        # sys.modules by the time B's setup.py is executed, causing B to run\n        # with the wrong versioneer. Setuptools wraps the sub-dep builds in a\n        # sandbox that restores sys.modules to it's pre-build state, so the\n        # parent is protected against the child's \"import versioneer\". By\n        # removing ourselves from sys.modules here, before the child build\n        # happens, we protect the child from the parent's versioneer too.\n        # Also see https://github.com/python-versioneer/python-versioneer/issues/52\n\n    cmds = {} if cmdclass is None else cmdclass.copy()\n\n    # we add \"version\" to setuptools\n    from setuptools import Command\n\n    class cmd_version(Command):\n        description = \"report generated version string\"\n        user_options: list[tuple[str, str, str]] = []\n        boolean_options: list[str] = []\n\n        def initialize_options(self) -> None:\n            pass\n\n        def finalize_options(self) -> None:\n            pass\n\n        def run(self) -> None:\n            vers = get_versions(verbose=True)\n            print(\"Version: %s\" % vers[\"version\"])\n            print(\" full-revisionid: %s\" % vers.get(\"full-revisionid\"))\n            print(\" dirty: %s\" % vers.get(\"dirty\"))\n            print(\" date: %s\" % vers.get(\"date\"))\n            if vers[\"error\"]:\n                print(\" error: %s\" % vers[\"error\"])\n    cmds[\"version\"] = cmd_version\n\n    # we override \"build_py\" in setuptools\n    #\n    # most invocation pathways end up running build_py:\n    #  distutils/build -> build_py\n    #  distutils/install -> distutils/build ->..\n    #  setuptools/bdist_wheel -> distutils/install ->..\n    #  setuptools/bdist_egg -> distutils/install_lib -> build_py\n    #  setuptools/install -> bdist_egg ->..\n    #  setuptools/develop -> ?\n    #  pip install:\n    #   copies source tree to a tempdir before running egg_info/etc\n    #   if .git isn't copied too, 'git describe' will fail\n    #   then does setup.py bdist_wheel, or sometimes setup.py install\n    #  setup.py egg_info -> ?\n\n    # pip install -e . and setuptool/editable_wheel will invoke build_py\n    # but the build_py command is not expected to copy any files.\n\n    # we override different \"build_py\" commands for both environments\n    if 'build_py' in cmds:\n        _build_py: Any = cmds['build_py']\n    else:\n        from setuptools.command.build_py import build_py as _build_py\n\n    class cmd_build_py(_build_py):\n        def run(self) -> None:\n            root = get_root()\n            cfg = get_config_from_root(root)\n            versions = get_versions()\n            _build_py.run(self)\n            if getattr(self, \"editable_mode\", False):\n                # During editable installs `.py` and data files are\n                # not copied to build_lib\n                return\n            # now locate _version.py in the new build/ directory and replace\n            # it with an updated value\n            if cfg.versionfile_build:\n                target_versionfile = os.path.join(self.build_lib,\n                                                  cfg.versionfile_build)\n                print(\"UPDATING %s\" % target_versionfile)\n                write_to_version_file(target_versionfile, versions)\n    cmds[\"build_py\"] = cmd_build_py\n\n    if 'build_ext' in cmds:\n        _build_ext: Any = cmds['build_ext']\n    else:\n        from setuptools.command.build_ext import build_ext as _build_ext\n\n    class cmd_build_ext(_build_ext):\n        def run(self) -> None:\n            root = get_root()\n            cfg = get_config_from_root(root)\n            versions = get_versions()\n            _build_ext.run(self)\n            if self.inplace:\n                # build_ext --inplace will only build extensions in\n                # build/lib<..> dir with no _version.py to write to.\n                # As in place builds will already have a _version.py\n                # in the module dir, we do not need to write one.\n                return\n            # now locate _version.py in the new build/ directory and replace\n            # it with an updated value\n            if not cfg.versionfile_build:\n                return\n            target_versionfile = os.path.join(self.build_lib,\n                                              cfg.versionfile_build)\n            if not os.path.exists(target_versionfile):\n                print(f\"Warning: {target_versionfile} does not exist, skipping \"\n                      \"version update. This can happen if you are running build_ext \"\n                      \"without first running build_py.\")\n                return\n            print(\"UPDATING %s\" % target_versionfile)\n            write_to_version_file(target_versionfile, versions)\n    cmds[\"build_ext\"] = cmd_build_ext\n\n    if \"cx_Freeze\" in sys.modules:  # cx_freeze enabled?\n        from cx_Freeze.dist import build_exe as _build_exe  # type: ignore\n        # nczeczulin reports that py2exe won't like the pep440-style string\n        # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g.\n        # setup(console=[{\n        #   \"version\": versioneer.get_version().split(\"+\", 1)[0], # FILEVERSION\n        #   \"product_version\": versioneer.get_version(),\n        #   ...\n\n        class cmd_build_exe(_build_exe):\n            def run(self) -> None:\n                root = get_root()\n                cfg = get_config_from_root(root)\n                versions = get_versions()\n                target_versionfile = cfg.versionfile_source\n                print(\"UPDATING %s\" % target_versionfile)\n                write_to_version_file(target_versionfile, versions)\n\n                _build_exe.run(self)\n                os.unlink(target_versionfile)\n                with open(cfg.versionfile_source, \"w\") as f:\n                    LONG = LONG_VERSION_PY[cfg.VCS]\n                    f.write(LONG %\n                            {\"DOLLAR\": \"$\",\n                             \"STYLE\": cfg.style,\n                             \"TAG_PREFIX\": cfg.tag_prefix,\n                             \"PARENTDIR_PREFIX\": cfg.parentdir_prefix,\n                             \"VERSIONFILE_SOURCE\": cfg.versionfile_source,\n                             })\n        cmds[\"build_exe\"] = cmd_build_exe\n        del cmds[\"build_py\"]\n\n    if 'py2exe' in sys.modules:  # py2exe enabled?\n        try:\n            from py2exe.setuptools_buildexe import py2exe as _py2exe  # type: ignore\n        except ImportError:\n            from py2exe.distutils_buildexe import py2exe as _py2exe  # type: ignore\n\n        class cmd_py2exe(_py2exe):\n            def run(self) -> None:\n                root = get_root()\n                cfg = get_config_from_root(root)\n                versions = get_versions()\n                target_versionfile = cfg.versionfile_source\n                print(\"UPDATING %s\" % target_versionfile)\n                write_to_version_file(target_versionfile, versions)\n\n                _py2exe.run(self)\n                os.unlink(target_versionfile)\n                with open(cfg.versionfile_source, \"w\") as f:\n                    LONG = LONG_VERSION_PY[cfg.VCS]\n                    f.write(LONG %\n                            {\"DOLLAR\": \"$\",\n                             \"STYLE\": cfg.style,\n                             \"TAG_PREFIX\": cfg.tag_prefix,\n                             \"PARENTDIR_PREFIX\": cfg.parentdir_prefix,\n                             \"VERSIONFILE_SOURCE\": cfg.versionfile_source,\n                             })\n        cmds[\"py2exe\"] = cmd_py2exe\n\n    # sdist farms its file list building out to egg_info\n    if 'egg_info' in cmds:\n        _egg_info: Any = cmds['egg_info']\n    else:\n        from setuptools.command.egg_info import egg_info as _egg_info\n\n    class cmd_egg_info(_egg_info):\n        def find_sources(self) -> None:\n            # egg_info.find_sources builds the manifest list and writes it\n            # in one shot\n            super().find_sources()\n\n            # Modify the filelist and normalize it\n            root = get_root()\n            cfg = get_config_from_root(root)\n            self.filelist.append('versioneer.py')\n            if cfg.versionfile_source:\n                # There are rare cases where versionfile_source might not be\n                # included by default, so we must be explicit\n                self.filelist.append(cfg.versionfile_source)\n            self.filelist.sort()\n            self.filelist.remove_duplicates()\n\n            # The write method is hidden in the manifest_maker instance that\n            # generated the filelist and was thrown away\n            # We will instead replicate their final normalization (to unicode,\n            # and POSIX-style paths)\n            from setuptools import unicode_utils\n            normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/')\n                          for f in self.filelist.files]\n\n            manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt')\n            with open(manifest_filename, 'w') as fobj:\n                fobj.write('\\n'.join(normalized))\n\n    cmds['egg_info'] = cmd_egg_info\n\n    # we override different \"sdist\" commands for both environments\n    if 'sdist' in cmds:\n        _sdist: Any = cmds['sdist']\n    else:\n        from setuptools.command.sdist import sdist as _sdist\n\n    class cmd_sdist(_sdist):\n        def run(self) -> None:\n            versions = get_versions()\n            self._versioneer_generated_versions = versions\n            # unless we update this, the command will keep using the old\n            # version\n            self.distribution.metadata.version = versions[\"version\"]\n            return _sdist.run(self)\n\n        def make_release_tree(self, base_dir: str, files: list[str]) -> None:\n            root = get_root()\n            cfg = get_config_from_root(root)\n            _sdist.make_release_tree(self, base_dir, files)\n            # now locate _version.py in the new base_dir directory\n            # (remembering that it may be a hardlink) and replace it with an\n            # updated value\n            target_versionfile = os.path.join(base_dir, cfg.versionfile_source)\n            print(\"UPDATING %s\" % target_versionfile)\n            write_to_version_file(target_versionfile,\n                                  self._versioneer_generated_versions)\n    cmds[\"sdist\"] = cmd_sdist\n\n    return cmds\n\n\nCONFIG_ERROR = \"\"\"\nsetup.cfg is missing the necessary Versioneer configuration. You need\na section like:\n\n [versioneer]\n VCS = git\n style = pep440\n versionfile_source = src/myproject/_version.py\n versionfile_build = myproject/_version.py\n tag_prefix =\n parentdir_prefix = myproject-\n\nYou will also need to edit your setup.py to use the results:\n\n import versioneer\n setup(version=versioneer.get_version(),\n       cmdclass=versioneer.get_cmdclass(), ...)\n\nPlease read the docstring in ./versioneer.py for configuration instructions,\nedit setup.cfg, and re-run the installer or 'python versioneer.py setup'.\n\"\"\"\n\nSAMPLE_CONFIG = \"\"\"\n# See the docstring in versioneer.py for instructions. Note that you must\n# re-run 'versioneer.py setup' after changing this section, and commit the\n# resulting files.\n\n[versioneer]\n#VCS = git\n#style = pep440\n#versionfile_source =\n#versionfile_build =\n#tag_prefix =\n#parentdir_prefix =\n\n\"\"\"\n\nOLD_SNIPPET = \"\"\"\nfrom ._version import get_versions\n__version__ = get_versions()['version']\ndel get_versions\n\"\"\"\n\nINIT_PY_SNIPPET = \"\"\"\nfrom . import {0}\n__version__ = {0}.get_versions()['version']\n\"\"\"\n\n\ndef do_setup() -> int:\n    \"\"\"Do main VCS-independent setup function for installing Versioneer.\"\"\"\n    root = get_root()\n    try:\n        cfg = get_config_from_root(root)\n    except (OSError, configparser.NoSectionError,\n            configparser.NoOptionError) as e:\n        if isinstance(e, (OSError, configparser.NoSectionError)):\n            print(\"Adding sample versioneer config to setup.cfg\",\n                  file=sys.stderr)\n            with open(os.path.join(root, \"setup.cfg\"), \"a\") as f:\n                f.write(SAMPLE_CONFIG)\n        print(CONFIG_ERROR, file=sys.stderr)\n        return 1\n\n    print(\" creating %s\" % cfg.versionfile_source)\n    with open(cfg.versionfile_source, \"w\") as f:\n        LONG = LONG_VERSION_PY[cfg.VCS]\n        f.write(LONG % {\"DOLLAR\": \"$\",\n                        \"STYLE\": cfg.style,\n                        \"TAG_PREFIX\": cfg.tag_prefix,\n                        \"PARENTDIR_PREFIX\": cfg.parentdir_prefix,\n                        \"VERSIONFILE_SOURCE\": cfg.versionfile_source,\n                        })\n\n    ipy = os.path.join(os.path.dirname(cfg.versionfile_source),\n                       \"__init__.py\")\n    maybe_ipy: Optional[str] = ipy\n    if os.path.exists(ipy):\n        try:\n            with open(ipy) as f:\n                old = f.read()\n        except OSError:\n            old = \"\"\n        module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0]\n        snippet = INIT_PY_SNIPPET.format(module)\n        if OLD_SNIPPET in old:\n            print(\" replacing boilerplate in %s\" % ipy)\n            with open(ipy, \"w\") as f:\n                f.write(old.replace(OLD_SNIPPET, snippet))\n        elif snippet not in old:\n            print(\" appending to %s\" % ipy)\n            with open(ipy, \"a\") as f:\n                f.write(snippet)\n        else:\n            print(\" %s unmodified\" % ipy)\n    else:\n        print(\" %s doesn't exist, ok\" % ipy)\n        maybe_ipy = None\n\n    # Make VCS-specific changes. For git, this means creating/changing\n    # .gitattributes to mark _version.py for export-subst keyword\n    # substitution.\n    do_vcs_install(cfg.versionfile_source, maybe_ipy)\n    return 0\n\n\ndef scan_setup_py() -> int:\n    \"\"\"Validate the contents of setup.py against Versioneer's expectations.\"\"\"\n    found = set()\n    setters = False\n    errors = 0\n    with open(\"setup.py\") as f:\n        for line in f.readlines():\n            if \"import versioneer\" in line:\n                found.add(\"import\")\n            if \"versioneer.get_cmdclass()\" in line:\n                found.add(\"cmdclass\")\n            if \"versioneer.get_version()\" in line:\n                found.add(\"get_version\")\n            if \"versioneer.VCS\" in line:\n                setters = True\n            if \"versioneer.versionfile_source\" in line:\n                setters = True\n    if len(found) != 3:\n        print(\"\")\n        print(\"Your setup.py appears to be missing some important items\")\n        print(\"(but I might be wrong). Please make sure it has something\")\n        print(\"roughly like the following:\")\n        print(\"\")\n        print(\" import versioneer\")\n        print(\" setup( version=versioneer.get_version(),\")\n        print(\"        cmdclass=versioneer.get_cmdclass(),  ...)\")\n        print(\"\")\n        errors += 1\n    if setters:\n        print(\"You should remove lines like 'versioneer.VCS = ' and\")\n        print(\"'versioneer.versionfile_source = ' . This configuration\")\n        print(\"now lives in setup.cfg, and should be removed from setup.py\")\n        print(\"\")\n        errors += 1\n    return errors\n\n\ndef setup_command() -> NoReturn:\n    \"\"\"Set up Versioneer and exit with appropriate error code.\"\"\"\n    errors = do_setup()\n    errors += scan_setup_py()\n    sys.exit(1 if errors else 0)\n\n\nif __name__ == \"__main__\":\n    cmd = sys.argv[1]\n    if cmd == \"setup\":\n        setup_command()\n"
  }
]