├── .gitignore ├── .pre-commit-config.yaml ├── README.md ├── poetry.lock ├── pyproject.toml ├── pytor ├── __init__.py ├── __main__.py ├── ed25519.py ├── onion.py └── test │ ├── __init__.py │ └── test_onions.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # End of https://www.gitignore.io/api/python 105 | 106 | # more 107 | key/ 108 | .vscode/* 109 | test/* 110 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: git://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.3.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-docstring-first 7 | - id: check-merge-conflict 8 | - id: check-yaml 9 | - id: end-of-file-fixer 10 | language_version: python3 11 | - id: requirements-txt-fixer 12 | - id: trailing-whitespace 13 | - repo: git://github.com/asottile/reorder_python_imports 14 | rev: v2.3.6 15 | hooks: 16 | - id: reorder-python-imports 17 | language_version: python3 18 | 19 | - repo: https://github.com/psf/black 20 | rev: 20.8b1 21 | hooks: 22 | - id: black 23 | args: 24 | - --line-length 25 | - '79' 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytor 2 | 3 | `pytor` is a simple python library to create and manager tor hidden services in version 2 and 3. 4 | 5 | 6 | ``` 7 | $ mkdir test 8 | $ pytor new-hidden-service test 9 | FYI: Binary data is base64 encoded 10 | path: 11 | test 12 | hostname: 13 | byb3bkhwi2ccbrctsqkowckpvk3tok36geddzg4l2m6yn6mrw626nqid.onion 14 | hs_ed25519_secret_key: 15 | PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAAAwIFsWaVtOk8r3RvXnkZcmxwIaDmmOdV8D7KaVf6yBWjVUIUTPpOWNQ9+hEiPKUclJ1RpflZ9FSdPgSj0j0tE3 16 | hs_ed25519_public_key: 17 | PT0gZWQyNTUxOXYxLXB1YmxpYzogdHlwZTAgPT0AAAAOA7Co9kaEIMRTlBTrCU+qtzcrfjEGPJuL0z2G+ZG3tQ== 18 | ``` 19 | 20 | Version 2 is is not working anymore on tor network but the toll can still created ones 21 | ```sh 22 | $ pytor new --version 2 23 | hostname: 24 | cljfodghi4w5frc6.onion 25 | private_key: 26 | -----BEGIN RSA PRIVATE KEY----- 27 | MIICXAIBAAKBgQD2Gza8HXzgDGo+YwyhjOdgD0GY7ti5en8YGXtcsBi/JIwHdKZo 28 | iLC4e5pWzlmB2ACdTw93ASFTGpPFs7nRxk4NuXo1BnvTsqzzcrsd9HV6xuKO7BkS 29 | aTEY3tgrSvB2nQtM1WR9FQoyxV+EWeE0Q9vrBVpEizO4kHqFXRanOJpJbwIDAQAB 30 | AoGAA/axPXteGP+qMGIJAIsT6OSmAlAKdoZGCL3UUkxVwbJVfQNAcNuOuRHojPBa 31 | 2bAAZogw8BI5Fq0NZzg7TGkctazKbvrmIx6o22spx2MOQXEc7lj3R3CJ8B+F1moz 32 | 9lNxIhNmw4bHeL3Sw5XMTPnOhCy1OKmouWrrcOj7B59YKrkCQQD2pWkZih6Ijl0y 33 | xG3vB8w22krpe0YOne94aXwdggkji6Cfne8YRNWU9y8FvxGZgwfXZKwGCOSOgq7r 34 | 0SP7vEoZAkEA/3CP8BGY1jThrLHLWNPKm5Vu1+YZClL4ibs4cdtxIs0J0l+dQcYW 35 | LMSkQpOy1C/nIIYPJpq9x8sCXG2BsRgwxwJAR9NhqONVAvVaZKdZUEuYB71IJXgV 36 | rboGe61UTI+Ks8Q8kV7/urSI8imNkwHSUT8cMHiLs/IxBOM/p0KvVOa/OQJAHlXY 37 | 0jLUysOW9XJb6t2kFxwFAODTonU+DOVOC796zR46h2BRhaknowNrWni96RMTSLqC 38 | /BuuZBbI3f8nQsfTqwJBAMX/KjXO/MqcB8TAjyKWHNyVR4T8OJM5lgbk8IGLKE5/ 39 | w96jWD0AEePqKKdWofLImi074zMSyMKuu6RFrkBSUuI= 40 | -----END RSA PRIVATE KEY----- 41 | ``` 42 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "astroid" 11 | version = "2.11.7" 12 | description = "An abstract syntax tree for Python with inference support." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.6.2" 16 | 17 | [package.dependencies] 18 | lazy-object-proxy = ">=1.4.0" 19 | typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 20 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} 21 | wrapt = ">=1.11,<2" 22 | 23 | [[package]] 24 | name = "atomicwrites" 25 | version = "1.4.1" 26 | description = "Atomic file writes." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 30 | 31 | [[package]] 32 | name = "attrs" 33 | version = "21.4.0" 34 | description = "Classes Without Boilerplate" 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 38 | 39 | [package.extras] 40 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 41 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 42 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 43 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 44 | 45 | [[package]] 46 | name = "black" 47 | version = "20.8b1" 48 | description = "The uncompromising code formatter." 49 | category = "dev" 50 | optional = false 51 | python-versions = ">=3.6" 52 | 53 | [package.dependencies] 54 | appdirs = "*" 55 | click = ">=7.1.2" 56 | dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} 57 | mypy-extensions = ">=0.4.3" 58 | pathspec = ">=0.6,<1" 59 | regex = ">=2020.1.8" 60 | toml = ">=0.10.1" 61 | typed-ast = ">=1.4.0" 62 | typing-extensions = ">=3.7.4" 63 | 64 | [package.extras] 65 | colorama = ["colorama (>=0.4.3)"] 66 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 67 | 68 | [[package]] 69 | name = "cfgv" 70 | version = "3.3.1" 71 | description = "Validate configuration and produce human readable error messages." 72 | category = "dev" 73 | optional = false 74 | python-versions = ">=3.6.1" 75 | 76 | [[package]] 77 | name = "click" 78 | version = "8.0.4" 79 | description = "Composable command line interface toolkit" 80 | category = "dev" 81 | optional = false 82 | python-versions = ">=3.6" 83 | 84 | [package.dependencies] 85 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 86 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 87 | 88 | [[package]] 89 | name = "colorama" 90 | version = "0.4.5" 91 | description = "Cross-platform colored terminal text." 92 | category = "dev" 93 | optional = false 94 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 95 | 96 | [[package]] 97 | name = "dataclasses" 98 | version = "0.8" 99 | description = "A backport of the dataclasses module for Python 3.6" 100 | category = "dev" 101 | optional = false 102 | python-versions = ">=3.6, <3.7" 103 | 104 | [[package]] 105 | name = "dill" 106 | version = "0.3.4" 107 | description = "serialize all of python" 108 | category = "dev" 109 | optional = false 110 | python-versions = ">=2.7, !=3.0.*" 111 | 112 | [package.extras] 113 | graph = ["objgraph (>=1.7.2)"] 114 | 115 | [[package]] 116 | name = "distlib" 117 | version = "0.3.5" 118 | description = "Distribution utilities" 119 | category = "dev" 120 | optional = false 121 | python-versions = "*" 122 | 123 | [[package]] 124 | name = "filelock" 125 | version = "3.4.1" 126 | description = "A platform independent file lock." 127 | category = "dev" 128 | optional = false 129 | python-versions = ">=3.6" 130 | 131 | [package.extras] 132 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 133 | testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 134 | 135 | [[package]] 136 | name = "fire" 137 | version = "0.4.0" 138 | description = "A library for automatically generating command line interfaces." 139 | category = "main" 140 | optional = false 141 | python-versions = "*" 142 | 143 | [package.dependencies] 144 | six = "*" 145 | termcolor = "*" 146 | 147 | [[package]] 148 | name = "identify" 149 | version = "2.4.4" 150 | description = "File identification library for Python" 151 | category = "dev" 152 | optional = false 153 | python-versions = ">=3.6.1" 154 | 155 | [package.extras] 156 | license = ["ukkonen"] 157 | 158 | [[package]] 159 | name = "importlib-metadata" 160 | version = "4.8.3" 161 | description = "Read metadata from Python packages" 162 | category = "dev" 163 | optional = false 164 | python-versions = ">=3.6" 165 | 166 | [package.dependencies] 167 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 168 | zipp = ">=0.5" 169 | 170 | [package.extras] 171 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 172 | perf = ["ipython"] 173 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 174 | 175 | [[package]] 176 | name = "importlib-resources" 177 | version = "5.2.3" 178 | description = "Read resources from Python packages" 179 | category = "dev" 180 | optional = false 181 | python-versions = ">=3.6" 182 | 183 | [package.dependencies] 184 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 185 | 186 | [package.extras] 187 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 188 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] 189 | 190 | [[package]] 191 | name = "iniconfig" 192 | version = "1.1.1" 193 | description = "iniconfig: brain-dead simple config-ini parsing" 194 | category = "dev" 195 | optional = false 196 | python-versions = "*" 197 | 198 | [[package]] 199 | name = "isort" 200 | version = "5.10.1" 201 | description = "A Python utility / library to sort Python imports." 202 | category = "dev" 203 | optional = false 204 | python-versions = ">=3.6.1,<4.0" 205 | 206 | [package.extras] 207 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 208 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 209 | colors = ["colorama (>=0.4.3,<0.5.0)"] 210 | plugins = ["setuptools"] 211 | 212 | [[package]] 213 | name = "jedi" 214 | version = "0.18.1" 215 | description = "An autocompletion tool for Python that can be used for text editors." 216 | category = "dev" 217 | optional = false 218 | python-versions = ">=3.6" 219 | 220 | [package.dependencies] 221 | parso = ">=0.8.0,<0.9.0" 222 | 223 | [package.extras] 224 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 225 | testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] 226 | 227 | [[package]] 228 | name = "lazy-object-proxy" 229 | version = "1.7.1" 230 | description = "A fast and thorough lazy object proxy." 231 | category = "dev" 232 | optional = false 233 | python-versions = ">=3.6" 234 | 235 | [[package]] 236 | name = "mccabe" 237 | version = "0.7.0" 238 | description = "McCabe checker, plugin for flake8" 239 | category = "dev" 240 | optional = false 241 | python-versions = ">=3.6" 242 | 243 | [[package]] 244 | name = "mypy-extensions" 245 | version = "0.4.3" 246 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 247 | category = "dev" 248 | optional = false 249 | python-versions = "*" 250 | 251 | [[package]] 252 | name = "nodeenv" 253 | version = "1.6.0" 254 | description = "Node.js virtual environment builder" 255 | category = "dev" 256 | optional = false 257 | python-versions = "*" 258 | 259 | [[package]] 260 | name = "packaging" 261 | version = "21.3" 262 | description = "Core utilities for Python packages" 263 | category = "dev" 264 | optional = false 265 | python-versions = ">=3.6" 266 | 267 | [package.dependencies] 268 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 269 | 270 | [[package]] 271 | name = "parso" 272 | version = "0.8.3" 273 | description = "A Python Parser" 274 | category = "dev" 275 | optional = false 276 | python-versions = ">=3.6" 277 | 278 | [package.extras] 279 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 280 | testing = ["docopt", "pytest (<6.0.0)"] 281 | 282 | [[package]] 283 | name = "pathspec" 284 | version = "0.9.0" 285 | description = "Utility library for gitignore style pattern matching of file paths." 286 | category = "dev" 287 | optional = false 288 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 289 | 290 | [[package]] 291 | name = "platformdirs" 292 | version = "2.4.0" 293 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 294 | category = "dev" 295 | optional = false 296 | python-versions = ">=3.6" 297 | 298 | [package.extras] 299 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 300 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 301 | 302 | [[package]] 303 | name = "pluggy" 304 | version = "1.0.0" 305 | description = "plugin and hook calling mechanisms for python" 306 | category = "dev" 307 | optional = false 308 | python-versions = ">=3.6" 309 | 310 | [package.dependencies] 311 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 312 | 313 | [package.extras] 314 | dev = ["pre-commit", "tox"] 315 | testing = ["pytest", "pytest-benchmark"] 316 | 317 | [[package]] 318 | name = "pre-commit" 319 | version = "2.17.0" 320 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 321 | category = "dev" 322 | optional = false 323 | python-versions = ">=3.6.1" 324 | 325 | [package.dependencies] 326 | cfgv = ">=2.0.0" 327 | identify = ">=1.0.0" 328 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 329 | importlib-resources = {version = "<5.3", markers = "python_version < \"3.7\""} 330 | nodeenv = ">=0.11.1" 331 | pyyaml = ">=5.1" 332 | toml = "*" 333 | virtualenv = ">=20.0.8" 334 | 335 | [[package]] 336 | name = "prompt-toolkit" 337 | version = "3.0.30" 338 | description = "Library for building powerful interactive command lines in Python" 339 | category = "dev" 340 | optional = false 341 | python-versions = ">=3.6.2" 342 | 343 | [package.dependencies] 344 | wcwidth = "*" 345 | 346 | [[package]] 347 | name = "ptpython" 348 | version = "3.0.20" 349 | description = "Python REPL build on top of prompt_toolkit" 350 | category = "dev" 351 | optional = false 352 | python-versions = ">=3.6" 353 | 354 | [package.dependencies] 355 | appdirs = "*" 356 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 357 | jedi = ">=0.16.0" 358 | prompt-toolkit = ">=3.0.18,<3.1.0" 359 | pygments = "*" 360 | 361 | [package.extras] 362 | all = ["black"] 363 | ptipython = ["ipython"] 364 | 365 | [[package]] 366 | name = "py" 367 | version = "1.11.0" 368 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 369 | category = "dev" 370 | optional = false 371 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 372 | 373 | [[package]] 374 | name = "pycodestyle" 375 | version = "2.8.0" 376 | description = "Python style guide checker" 377 | category = "dev" 378 | optional = false 379 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 380 | 381 | [[package]] 382 | name = "pycryptodome" 383 | version = "3.15.0" 384 | description = "Cryptographic library for Python" 385 | category = "main" 386 | optional = false 387 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 388 | 389 | [[package]] 390 | name = "pydocstyle" 391 | version = "6.1.1" 392 | description = "Python docstring style checker" 393 | category = "dev" 394 | optional = false 395 | python-versions = ">=3.6" 396 | 397 | [package.dependencies] 398 | snowballstemmer = "*" 399 | 400 | [package.extras] 401 | toml = ["toml"] 402 | 403 | [[package]] 404 | name = "pyfakefs" 405 | version = "4.6.3" 406 | description = "pyfakefs implements a fake file system that mocks the Python file system modules." 407 | category = "dev" 408 | optional = false 409 | python-versions = ">=3.6" 410 | 411 | [[package]] 412 | name = "pyflakes" 413 | version = "2.4.0" 414 | description = "passive checker of Python programs" 415 | category = "dev" 416 | optional = false 417 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 418 | 419 | [[package]] 420 | name = "pygments" 421 | version = "2.12.0" 422 | description = "Pygments is a syntax highlighting package written in Python." 423 | category = "dev" 424 | optional = false 425 | python-versions = ">=3.6" 426 | 427 | [[package]] 428 | name = "pylama" 429 | version = "7.7.1" 430 | description = "pylama -- Code audit tool for python" 431 | category = "dev" 432 | optional = false 433 | python-versions = "*" 434 | 435 | [package.dependencies] 436 | mccabe = ">=0.5.2" 437 | pycodestyle = ">=2.3.1" 438 | pydocstyle = ">=2.0.0" 439 | pyflakes = ">=1.5.0" 440 | 441 | [[package]] 442 | name = "pylint" 443 | version = "2.13.9" 444 | description = "python code static checker" 445 | category = "dev" 446 | optional = false 447 | python-versions = ">=3.6.2" 448 | 449 | [package.dependencies] 450 | astroid = ">=2.11.5,<=2.12.0-dev0" 451 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 452 | dill = ">=0.2" 453 | isort = ">=4.2.5,<6" 454 | mccabe = ">=0.6,<0.8" 455 | platformdirs = ">=2.2.0" 456 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 457 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 458 | 459 | [package.extras] 460 | testutil = ["gitpython (>3)"] 461 | 462 | [[package]] 463 | name = "pyparsing" 464 | version = "3.0.9" 465 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 466 | category = "dev" 467 | optional = false 468 | python-versions = ">=3.6.8" 469 | 470 | [package.extras] 471 | diagrams = ["railroad-diagrams", "jinja2"] 472 | 473 | [[package]] 474 | name = "pytest" 475 | version = "7.0.1" 476 | description = "pytest: simple powerful testing with Python" 477 | category = "dev" 478 | optional = false 479 | python-versions = ">=3.6" 480 | 481 | [package.dependencies] 482 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 483 | attrs = ">=19.2.0" 484 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 485 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 486 | iniconfig = "*" 487 | packaging = "*" 488 | pluggy = ">=0.12,<2.0" 489 | py = ">=1.8.2" 490 | tomli = ">=1.0.0" 491 | 492 | [package.extras] 493 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 494 | 495 | [[package]] 496 | name = "pyyaml" 497 | version = "6.0" 498 | description = "YAML parser and emitter for Python" 499 | category = "main" 500 | optional = false 501 | python-versions = ">=3.6" 502 | 503 | [[package]] 504 | name = "regex" 505 | version = "2022.7.9" 506 | description = "Alternative regular expression module, to replace re." 507 | category = "dev" 508 | optional = false 509 | python-versions = ">=3.6" 510 | 511 | [[package]] 512 | name = "six" 513 | version = "1.16.0" 514 | description = "Python 2 and 3 compatibility utilities" 515 | category = "main" 516 | optional = false 517 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 518 | 519 | [[package]] 520 | name = "snowballstemmer" 521 | version = "2.2.0" 522 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 523 | category = "dev" 524 | optional = false 525 | python-versions = "*" 526 | 527 | [[package]] 528 | name = "termcolor" 529 | version = "1.1.0" 530 | description = "ANSII Color formatting for output in terminal." 531 | category = "main" 532 | optional = false 533 | python-versions = "*" 534 | 535 | [[package]] 536 | name = "toml" 537 | version = "0.10.2" 538 | description = "Python Library for Tom's Obvious, Minimal Language" 539 | category = "dev" 540 | optional = false 541 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 542 | 543 | [[package]] 544 | name = "tomli" 545 | version = "1.2.3" 546 | description = "A lil' TOML parser" 547 | category = "dev" 548 | optional = false 549 | python-versions = ">=3.6" 550 | 551 | [[package]] 552 | name = "tox" 553 | version = "3.25.1" 554 | description = "tox is a generic virtualenv management and test command line tool" 555 | category = "dev" 556 | optional = false 557 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 558 | 559 | [package.dependencies] 560 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 561 | filelock = ">=3.0.0" 562 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 563 | packaging = ">=14" 564 | pluggy = ">=0.12.0" 565 | py = ">=1.4.17" 566 | six = ">=1.14.0" 567 | toml = ">=0.9.4" 568 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 569 | 570 | [package.extras] 571 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 572 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] 573 | 574 | [[package]] 575 | name = "typed-ast" 576 | version = "1.5.4" 577 | description = "a fork of Python 2 and 3 ast modules with type comment support" 578 | category = "dev" 579 | optional = false 580 | python-versions = ">=3.6" 581 | 582 | [[package]] 583 | name = "typing-extensions" 584 | version = "4.1.1" 585 | description = "Backported and Experimental Type Hints for Python 3.6+" 586 | category = "dev" 587 | optional = false 588 | python-versions = ">=3.6" 589 | 590 | [[package]] 591 | name = "virtualenv" 592 | version = "20.15.1" 593 | description = "Virtual Python Environment builder" 594 | category = "dev" 595 | optional = false 596 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 597 | 598 | [package.dependencies] 599 | distlib = ">=0.3.1,<1" 600 | filelock = ">=3.2,<4" 601 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 602 | importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} 603 | platformdirs = ">=2,<3" 604 | six = ">=1.9.0,<2" 605 | 606 | [package.extras] 607 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 608 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 609 | 610 | [[package]] 611 | name = "wcwidth" 612 | version = "0.2.5" 613 | description = "Measures the displayed width of unicode strings in a terminal" 614 | category = "dev" 615 | optional = false 616 | python-versions = "*" 617 | 618 | [[package]] 619 | name = "wrapt" 620 | version = "1.14.1" 621 | description = "Module for decorators, wrappers and monkey patching." 622 | category = "dev" 623 | optional = false 624 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 625 | 626 | [[package]] 627 | name = "zipp" 628 | version = "3.6.0" 629 | description = "Backport of pathlib-compatible object wrapper for zip files" 630 | category = "dev" 631 | optional = false 632 | python-versions = ">=3.6" 633 | 634 | [package.extras] 635 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 636 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 637 | 638 | [metadata] 639 | lock-version = "1.1" 640 | python-versions = ">= 3.6.12, < 3.10" 641 | content-hash = "9ea6cbab84472f2b7bcdc2c36063e3f5cb0ad442f14cf7f8bdb85852866542e1" 642 | 643 | [metadata.files] 644 | appdirs = [ 645 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 646 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 647 | ] 648 | astroid = [] 649 | atomicwrites = [] 650 | attrs = [] 651 | black = [ 652 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 653 | ] 654 | cfgv = [ 655 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 656 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 657 | ] 658 | click = [] 659 | colorama = [] 660 | dataclasses = [ 661 | {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, 662 | {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, 663 | ] 664 | dill = [] 665 | distlib = [] 666 | filelock = [] 667 | fire = [ 668 | {file = "fire-0.4.0.tar.gz", hash = "sha256:c5e2b8763699d1142393a46d0e3e790c5eb2f0706082df8f647878842c216a62"}, 669 | ] 670 | identify = [] 671 | importlib-metadata = [ 672 | {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"}, 673 | {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"}, 674 | ] 675 | importlib-resources = [ 676 | {file = "importlib_resources-5.2.3-py3-none-any.whl", hash = "sha256:ae35ed1cfe8c0d6c1a53ecd168167f01fa93b893d51a62cdf23aea044c67211b"}, 677 | {file = "importlib_resources-5.2.3.tar.gz", hash = "sha256:203d70dda34cfbfbb42324a8d4211196e7d3e858de21a5eb68c6d1cdd99e4e98"}, 678 | ] 679 | iniconfig = [ 680 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 681 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 682 | ] 683 | isort = [ 684 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 685 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 686 | ] 687 | jedi = [ 688 | {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, 689 | {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, 690 | ] 691 | lazy-object-proxy = [ 692 | {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, 693 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, 694 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, 695 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, 696 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, 697 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, 698 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, 699 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, 700 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, 701 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, 702 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, 703 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, 704 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, 705 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, 706 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, 707 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, 708 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, 709 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, 710 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, 711 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, 712 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, 713 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, 714 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, 715 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, 716 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, 717 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, 718 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, 719 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, 720 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, 721 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, 722 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, 723 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, 724 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, 725 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, 726 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, 727 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, 728 | {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, 729 | ] 730 | mccabe = [] 731 | mypy-extensions = [ 732 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 733 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 734 | ] 735 | nodeenv = [ 736 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 737 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 738 | ] 739 | packaging = [ 740 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 741 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 742 | ] 743 | parso = [ 744 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 745 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 746 | ] 747 | pathspec = [ 748 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 749 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 750 | ] 751 | platformdirs = [ 752 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 753 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 754 | ] 755 | pluggy = [ 756 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 757 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 758 | ] 759 | pre-commit = [] 760 | prompt-toolkit = [] 761 | ptpython = [ 762 | {file = "ptpython-3.0.20-py2.py3-none-any.whl", hash = "sha256:99636899ab0e4d026e2ecc9368269114f387b4bb5411e57f072b0bde724d9f99"}, 763 | {file = "ptpython-3.0.20.tar.gz", hash = "sha256:eafd4ced27ca5dc370881d4358d1ab5041b32d88d31af8e3c24167fe4af64ed6"}, 764 | ] 765 | py = [ 766 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 767 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 768 | ] 769 | pycodestyle = [ 770 | {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, 771 | {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, 772 | ] 773 | pycryptodome = [] 774 | pydocstyle = [ 775 | {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, 776 | {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, 777 | ] 778 | pyfakefs = [] 779 | pyflakes = [ 780 | {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, 781 | {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, 782 | ] 783 | pygments = [ 784 | {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, 785 | {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, 786 | ] 787 | pylama = [ 788 | {file = "pylama-7.7.1-py2.py3-none-any.whl", hash = "sha256:fd61c11872d6256b019ef1235be37b77c922ef37ac9797df6bd489996dddeb15"}, 789 | {file = "pylama-7.7.1.tar.gz", hash = "sha256:9bae53ef9c1a431371d6a8dca406816a60d547147b60a4934721898f553b7d8f"}, 790 | ] 791 | pylint = [] 792 | pyparsing = [ 793 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 794 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 795 | ] 796 | pytest = [] 797 | pyyaml = [ 798 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 799 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 800 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 801 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 802 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 803 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 804 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 805 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 806 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 807 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 808 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 809 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 810 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 811 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 812 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 813 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 814 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 815 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 816 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 817 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 818 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 819 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 820 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 821 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 822 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 823 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 824 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 825 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 826 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 827 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 828 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 829 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 830 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 831 | ] 832 | regex = [] 833 | six = [ 834 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 835 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 836 | ] 837 | snowballstemmer = [ 838 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 839 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 840 | ] 841 | termcolor = [ 842 | {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, 843 | ] 844 | toml = [ 845 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 846 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 847 | ] 848 | tomli = [] 849 | tox = [] 850 | typed-ast = [ 851 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, 852 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, 853 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, 854 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, 855 | {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, 856 | {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, 857 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, 858 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, 859 | {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, 860 | {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, 861 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, 862 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, 863 | {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, 864 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, 865 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, 866 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, 867 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, 868 | {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, 869 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, 870 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, 871 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, 872 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, 873 | {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, 874 | {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, 875 | ] 876 | typing-extensions = [] 877 | virtualenv = [] 878 | wcwidth = [ 879 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 880 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 881 | ] 882 | wrapt = [ 883 | {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, 884 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, 885 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, 886 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, 887 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, 888 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, 889 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, 890 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, 891 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, 892 | {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, 893 | {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, 894 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, 895 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, 896 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, 897 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, 898 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, 899 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, 900 | {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, 901 | {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, 902 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, 903 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, 904 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, 905 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, 906 | {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, 907 | {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, 908 | {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, 909 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, 910 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, 911 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, 912 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, 913 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, 914 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, 915 | {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, 916 | {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, 917 | {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, 918 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, 919 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, 920 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, 921 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, 922 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, 923 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, 924 | {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, 925 | {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, 926 | {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, 927 | {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, 928 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, 929 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, 930 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, 931 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, 932 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, 933 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, 934 | {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, 935 | {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, 936 | {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, 937 | {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, 938 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, 939 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, 940 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, 941 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, 942 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, 943 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, 944 | {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, 945 | {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, 946 | {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, 947 | ] 948 | zipp = [ 949 | {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, 950 | {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, 951 | ] 952 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool] 2 | [tool.poetry] 3 | name = "pytor" 4 | version = "0.1.9" 5 | description = "Manage Tor hidden services keys" 6 | license = "WTFPL" 7 | classifiers = ["Programming Language :: Python", "Development Status :: 1 - Planning", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"] 8 | homepage = "http://github.com/cmehay/pytor" 9 | repository = "https://pypi.org/project/pytor/" 10 | authors = ["Christophe Mehay "] 11 | 12 | [tool.poetry.dependencies] 13 | python = ">= 3.6.12, < 3.11" 14 | pycryptodome = ">=3.9.4" 15 | fire = ">=0.3.1" 16 | pyyaml = ">=5.3.1" 17 | 18 | [tool.poetry.dev-dependencies] 19 | tox = "*" 20 | pre-commit = ">=2.8.2" 21 | ptpython = "*" 22 | pylama = "*" 23 | pylint = "*" 24 | pytest = "*" 25 | black = "^20.8b1" 26 | pyfakefs = "^4.3.0" 27 | 28 | [tool.poetry.scripts] 29 | pytor = "pytor.__main__:main" 30 | 31 | [build-system] 32 | requires = [ 33 | "poetry >= 1.0.9", 34 | ] 35 | build-backend = "poetry.masonry.api" 36 | -------------------------------------------------------------------------------- /pytor/__init__.py: -------------------------------------------------------------------------------- 1 | from .onion import OnionV2 2 | from .onion import OnionV3 3 | 4 | __all__ = [ 5 | "OnionV2", 6 | "OnionV3", 7 | ] 8 | -------------------------------------------------------------------------------- /pytor/__main__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | import fire 5 | import yaml 6 | 7 | from .onion import NonEmptyDirException 8 | from .onion import OnionV2 9 | from .onion import OnionV3 10 | 11 | 12 | class Format(object): 13 | def __init__(self, format: str): 14 | attr = "print_{format}".format(format=format) 15 | if not hasattr(self, attr): 16 | raise Exception("Output format not valid") 17 | self.method = getattr(self, attr) 18 | print("FYI: Binary data is base64 encoded", file=sys.stderr) 19 | 20 | def print(self, data: dict): 21 | self.method(data) 22 | 23 | def print_plain(self, data: dict): 24 | for key, value in data.items(): 25 | print("{key}:\n{value}".format(key=key, value=value)) 26 | 27 | def print_json(self, data: dict): 28 | print(json.dumps(data, indent=4)) 29 | 30 | def print_yaml(self, data: dict): 31 | print(yaml.dump(data)) 32 | 33 | 34 | class Pytor(object): 35 | 36 | _obj = { 37 | 2: OnionV2, 38 | 3: OnionV3, 39 | } 40 | 41 | def __init__(self, version: int = 3, format: str = "plain"): 42 | if version not in self._obj: 43 | raise Exception("Onion version not valid") 44 | self._version = version 45 | self._print = Format(format).print 46 | self._stderr: lambda x: print(x, file=sys.stderr) 47 | 48 | @property 49 | def _cls(self): 50 | return self._obj[self._version] 51 | 52 | def new(self): 53 | obj = self._cls() 54 | self._print(obj.serialize()) 55 | 56 | def new_hidden_service(self, path: str, force: bool = False): 57 | obj = self._cls() 58 | try: 59 | obj.write_hidden_service(path=path, force=force) 60 | except NonEmptyDirException: 61 | s = input( 62 | "Dir {path} not empty, override? [Y/n]".format(path=path) 63 | ) 64 | if not s or s.lower() == "y": 65 | obj.write_hidden_service(path=path, force=True) 66 | else: 67 | print("Canceled...") 68 | self._print({"path": path, **obj.serialize()}) 69 | 70 | 71 | def main(): 72 | fire.Fire(Pytor) 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /pytor/ed25519.py: -------------------------------------------------------------------------------- 1 | """ 2 | this code is a cleaned version of http://ed25519.cr.yp.to/python/ed25519.py 3 | for python3 4 | 5 | code released under the terms of the GNU Public License v3, copyleft 2015 6 | yoochan 7 | """ 8 | import collections 9 | 10 | Point = collections.namedtuple("Point", ["x", "y"]) 11 | 12 | 13 | class Ed25519: 14 | """ 15 | Generate public key from private key secret hash 16 | """ 17 | 18 | length = 256 19 | 20 | def __init__(self): 21 | self.q = 2 ** 255 - 19 22 | self.l = 2 ** 252 + 27742317777372353535851937790883648493 23 | self.d = -121665 * self.inverse(121666) 24 | 25 | self.i = pow(2, (self.q - 1) // 4, self.q) 26 | 27 | self.B = self.point(4 * self.inverse(5)) 28 | 29 | def from_bytes(self, h): 30 | """ pick 32 bytes, return a 256 bit int """ 31 | return int.from_bytes(h[0 : self.length // 8], "little", signed=False) 32 | 33 | def to_bytes(self, k): 34 | return k.to_bytes(self.length // 8, "little", signed=False) 35 | 36 | def public_key_from_hash(self, hash): 37 | c = self.outer(self.B, int.from_bytes(hash[:32], "little")) 38 | return self.point_to_bytes(c) 39 | 40 | def inverse(self, x): 41 | return pow(x, self.q - 2, self.q) 42 | 43 | def recover(self, y): 44 | """ given a value y, recover the preimage x """ 45 | p = (y * y - 1) * self.inverse(self.d * y * y + 1) 46 | x = pow(p, (self.q + 3) // 8, self.q) 47 | if (x * x - p) % self.q != 0: 48 | x = (x * self.i) % self.q 49 | if x % 2 != 0: 50 | x = self.q - x 51 | return x 52 | 53 | def point(self, y): 54 | """ given a value y, recover x and return the corresponding P(x, y) """ 55 | return Point(self.recover(y) % self.q, y % self.q) 56 | 57 | def inner(self, P, Q): 58 | """ inner product on the curve, between two points """ 59 | x = (P.x * Q.y + Q.x * P.y) * self.inverse( 60 | 1 + self.d * P.x * Q.x * P.y * Q.y 61 | ) 62 | y = (P.y * Q.y + P.x * Q.x) * self.inverse( 63 | 1 - self.d * P.x * Q.x * P.y * Q.y 64 | ) 65 | return Point(x % self.q, y % self.q) 66 | 67 | def outer(self, P, n): 68 | """ outer product on the curve, between a point and a scalar """ 69 | if n == 0: 70 | return Point(0, 1) 71 | Q = self.outer(P, n // 2) 72 | Q = self.inner(Q, Q) 73 | if n & 1: 74 | Q = self.inner(Q, P) 75 | return Q 76 | 77 | def point_to_bytes(self, P): 78 | return (P.y + ((P.x & 1) << 255)).to_bytes(self.length // 8, "little") 79 | -------------------------------------------------------------------------------- /pytor/onion.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import warnings 4 | from abc import ABC 5 | from abc import abstractmethod 6 | from base64 import b32encode 7 | from base64 import b64encode 8 | from hashlib import sha1 9 | from hashlib import sha3_256 10 | from hashlib import sha512 11 | from typing import BinaryIO 12 | 13 | from Crypto.PublicKey import RSA 14 | from Crypto.Random import get_random_bytes 15 | 16 | from .ed25519 import Ed25519 17 | 18 | 19 | __all__ = [ 20 | "OnionV2", 21 | "OnionV3", 22 | "EmptyDirException", 23 | "NonEmptyDirException", 24 | ] 25 | 26 | 27 | class EmptyDirException(Exception): 28 | pass 29 | 30 | 31 | class NonEmptyDirException(Exception): 32 | pass 33 | 34 | 35 | class Onion(ABC): 36 | """ 37 | Interface to implement hidden services keys managment 38 | """ 39 | 40 | _priv = None 41 | _pub = None 42 | _hidden_service_path = None 43 | _priv_key_filename = None 44 | _pub_key_filename = None 45 | _host_filename = None 46 | _version = None 47 | 48 | def __init__( 49 | self, private_key: bytes = None, hidden_service_path: str = None 50 | ): 51 | if self._version == 2: 52 | warnings.warn( 53 | "Onion addresses version 2 are not supported anymore by tor", 54 | UserWarning 55 | ) 56 | 57 | if hidden_service_path: 58 | try: 59 | self.load_hidden_service(hidden_service_path) 60 | except EmptyDirException: 61 | pass 62 | self._hidden_service_path = hidden_service_path 63 | if private_key: 64 | self.set_private_key(private_key) 65 | if not self._priv: 66 | self.gen_new_private_key() 67 | 68 | @abstractmethod 69 | def gen_new_private_key(self) -> None: 70 | "Generate new private key" 71 | ... 72 | 73 | def set_private_key_from_file(self, file: BinaryIO): 74 | "Load private key from file" 75 | self.set_private_key(file.read()) 76 | 77 | @abstractmethod 78 | def set_private_key(self, key: bytes) -> None: 79 | "Add private key" 80 | ... 81 | 82 | @abstractmethod 83 | def _save_keypair(self, key) -> None: 84 | "Generate pub key from priv key and save both in instance" 85 | ... 86 | 87 | def load_hidden_service(self, path: str) -> None: 88 | if not os.path.isdir(path): 89 | raise Exception( 90 | "{path} should be an existing directory".format(path=path) 91 | ) 92 | if self._priv_key_filename not in os.listdir(path): 93 | raise EmptyDirException( 94 | "{key} file not found in {path}".format( 95 | key=self._priv_key_filename, path=path 96 | ) 97 | ) 98 | with open(os.path.join(path, self._priv_key_filename), "rb") as f: 99 | self.set_private_key_from_file(f) 100 | 101 | def write_hidden_service( 102 | self, path: str = None, force: bool = False 103 | ) -> None: 104 | path = path or self._hidden_service_path 105 | if not path: 106 | raise Exception("Missing hidden service path") 107 | if not os.path.exists(path): 108 | raise Exception( 109 | "{path} should be an existing directory".format(path=path) 110 | ) 111 | if ( 112 | os.path.exists(os.path.join(path, self._host_filename)) 113 | or os.path.exists(os.path.join(path, self._priv_key_filename)) 114 | ) and not force: 115 | raise NonEmptyDirException( 116 | "Use force=True for non empty hidden service directory" 117 | ) 118 | with open(os.path.join(path, self._priv_key_filename), "wb") as f: 119 | f.write(self._get_private_key_has_native()) 120 | with open(os.path.join(path, self._host_filename), "w") as f: 121 | f.write(self.onion_hostname) 122 | 123 | def get_available_private_key_formats(self) -> list: 124 | "Get private key export availables formats" 125 | r = re.compile(r"_get_private_key_has_([a-z]+)") 126 | formats = [] 127 | for method in dir(self): 128 | m = r.match(method) 129 | if m: 130 | formats.append(m[1]) 131 | return formats 132 | 133 | def get_private_key(self, format: str = "native"): 134 | "Get the private key as specified format" 135 | method = "_get_private_key_has_{format}".format(format=format) 136 | if not hasattr(self, method) and not callable(getattr(self, method)): 137 | raise NotImplementedError("Method {method} if not implemented") 138 | return getattr(self, method)() 139 | 140 | @abstractmethod 141 | def _get_private_key_has_native(self) -> bytes: 142 | "Get private key like in tor native format" 143 | ... 144 | 145 | @abstractmethod 146 | def get_public_key(self) -> bytes: 147 | "Compute public key" 148 | if not self._priv: 149 | raise Exception("No private key has been set") 150 | 151 | @abstractmethod 152 | def get_onion_str(self) -> str: 153 | "Compute onion address string" 154 | ... 155 | 156 | @property 157 | def onion_hostname(self) -> str: 158 | return "{onion}.onion".format(onion=self.get_onion_str()) 159 | 160 | @property 161 | def version(self) -> str: 162 | return str(self._version) 163 | 164 | 165 | class OnionV2(Onion): 166 | """ 167 | Tor onion address v2 implement 168 | """ 169 | 170 | _priv_key_filename = "private_key" 171 | _host_filename = "hostname" 172 | 173 | _version = 2 174 | 175 | def gen_new_private_key(self) -> None: 176 | "Generate new 1024 bits RSA key for hidden service" 177 | self._save_keypair(RSA.generate(bits=1024)) 178 | 179 | def _save_keypair(self, key: RSA.RsaKey) -> None: 180 | self._priv = key.exportKey("PEM") 181 | self._pub = key.publickey().exportKey("DER") 182 | 183 | def set_private_key(self, key: bytes) -> None: 184 | "Add private key" 185 | if not key.startswith(b"-----BEGIN RSA PRIVATE KEY-----"): 186 | raise Exception( 187 | "Private key does not seems to be a valid RSA PEM key" 188 | ) 189 | self._save_keypair(RSA.importKey(key.strip())) 190 | 191 | def _get_private_key_has_native(self) -> bytes: 192 | "Get RSA private key like in PEM" 193 | return self._get_private_key_has_pem() 194 | 195 | def _get_private_key_has_pem(self) -> bytes: 196 | "Get RSA private key like in PEM" 197 | return RSA.importKey(self._priv).exportKey("PEM") 198 | 199 | def get_public_key(self) -> bytes: 200 | "Compute public key" 201 | super().get_public_key() 202 | return self._pub 203 | 204 | def get_onion_str(self) -> str: 205 | "Compute onion address string" 206 | return b32encode(sha1(self._pub[22:]).digest()[:10]).decode().lower() 207 | 208 | def serialize(self): 209 | return { 210 | self._host_filename: self.onion_hostname, 211 | self._priv_key_filename: self.get_private_key().decode(), 212 | } 213 | 214 | 215 | class OnionV3(Onion): 216 | """ 217 | Tor onion address v3 implement 218 | """ 219 | 220 | _header_priv = b"== ed25519v1-secret: type0 ==\x00\x00\x00" 221 | _header_pub = b"== ed25519v1-public: type0 ==\x00\x00\x00" 222 | 223 | _priv_key_filename = "hs_ed25519_secret_key" 224 | _pub_key_filename = "hs_ed25519_public_key" 225 | _host_filename = "hostname" 226 | 227 | _version = 3 228 | 229 | def _save_keypair(self, key: bytes) -> None: 230 | self._priv = key 231 | self._pub = Ed25519().public_key_from_hash(key) 232 | 233 | def gen_new_private_key(self) -> None: 234 | "Generate new tor ed25519 512 bits key" 235 | random = get_random_bytes(32) 236 | key = bytearray(sha512(random).digest()) 237 | key[0] &= 248 238 | key[31] &= 63 239 | key[31] |= 64 240 | self._save_keypair(bytes(key)) 241 | 242 | def set_private_key(self, key: bytes) -> None: 243 | "Add private key" 244 | if not key.startswith(self._header_priv): 245 | raise Exception( 246 | "Private key does not seems to be a valid ed25519 tor key" 247 | ) 248 | parsed_key = key[32:] 249 | if len(parsed_key) != 64: 250 | raise Exception( 251 | "Private key does not seem to have the good lenght" 252 | ) 253 | self._save_keypair(parsed_key) 254 | 255 | def set_private_key_from_file(self, file: BinaryIO): 256 | "Load private key from file" 257 | self.set_private_key(file.read()) 258 | 259 | def _get_private_key_has_native(self) -> bytes: 260 | "Get RSA private key like in PEM" 261 | return self._header_priv + self._priv 262 | 263 | def get_public_key(self) -> bytes: 264 | "Compute public key" 265 | super().get_public_key() 266 | return self._header_pub + self._pub 267 | 268 | def write_hidden_service( 269 | self, path: str = None, force: bool = False 270 | ) -> None: 271 | path = path or self._hidden_service_path 272 | super().write_hidden_service(path, force) 273 | with open(os.path.join(path, self._pub_key_filename), "wb") as f: 274 | f.write(self.get_public_key()) 275 | 276 | def get_onion_str(self) -> str: 277 | "Compute onion address string" 278 | version_byte = b"\x03" 279 | 280 | def checksum(pubkey): 281 | checksum_str = ".onion checksum".encode("ascii") 282 | return sha3_256(checksum_str + self._pub + version_byte).digest()[ 283 | :2 284 | ] 285 | 286 | return ( 287 | b32encode(self._pub + checksum(self._pub) + version_byte) 288 | .decode() 289 | .lower() 290 | ) 291 | 292 | def serialize(self): 293 | return { 294 | self._host_filename: self.onion_hostname, 295 | self._priv_key_filename: b64encode( 296 | self.get_private_key() 297 | ).decode(), 298 | self._pub_key_filename: b64encode(self.get_public_key()).decode(), 299 | } 300 | -------------------------------------------------------------------------------- /pytor/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmehay/pytor/817d4344d582cd9432909989c7a2720351adaa4c/pytor/test/__init__.py -------------------------------------------------------------------------------- /pytor/test/test_onions.py: -------------------------------------------------------------------------------- 1 | import os 2 | from base64 import b64decode 3 | 4 | import pytest 5 | 6 | from ..onion import OnionV2 7 | from ..onion import OnionV3 8 | 9 | 10 | class _testOnion: 11 | 12 | private_key = None 13 | public_key = None 14 | onion_hostname = None 15 | onion_cls = None 16 | version = None 17 | 18 | check_method = { 19 | "priv": "get_private_key", 20 | "pub": "get_public_key", 21 | "onion": "onion_hostname", 22 | } 23 | 24 | def generate_new_key(self): 25 | return self.onion_cls().get_private_key() 26 | 27 | def test_import_key(self): 28 | o = self.onion_cls(private_key=self.private_key) 29 | assert o.get_public_key() == self.public_key 30 | assert o.onion_hostname == self.onion_hostname 31 | assert o.get_private_key() == self.private_key 32 | 33 | def test_generate_key(self): 34 | o = self.onion_cls() 35 | assert o.onion_hostname.endswith(".onion") 36 | assert len(o.onion_hostname) == len(self.onion_hostname) 37 | 38 | def test_import_file(self, fs): 39 | path = os.path.join("/test_tor", self.files["priv"]) 40 | fs.create_file(path, contents=self.private_key) 41 | 42 | o = self.onion_cls() 43 | path = os.path.join("/test_tor", self.files["priv"]) 44 | with open(path, "rb") as f: 45 | o.set_private_key_from_file(f) 46 | assert o.onion_hostname == self.onion_hostname 47 | 48 | def test_import_hidden_directory(self, tmpdir): 49 | d = tmpdir.mkdir("hidden_directory") 50 | f = d.join(self.files["priv"]) 51 | f.write_binary(self.private_key) 52 | o = self.onion_cls(hidden_service_path=d) 53 | assert o.onion_hostname == self.onion_hostname 54 | 55 | def test_write_hidden_directory(self, tmpdir): 56 | d = tmpdir.mkdir("hidden_directory") 57 | o = self.onion_cls(private_key=self.private_key) 58 | o.write_hidden_service(path=str(d)) 59 | 60 | for file_type, file in self.files.items(): 61 | method = getattr(o, self.check_method[file_type]) 62 | check = method() if callable(method) else method 63 | check = check.encode() if isinstance(check, str) else check 64 | assert d.join(file).read_binary() == check 65 | 66 | def test_import_empty_hidden_directory(self, tmpdir): 67 | d = tmpdir.mkdir("hidden_directory") 68 | o = self.onion_cls(hidden_service_path=d) 69 | o.write_hidden_service() 70 | assert d.join(self.files["priv"]).read_binary() == o.get_private_key() 71 | assert d.join(self.files["onion"]).read() == o.onion_hostname 72 | 73 | def test_import_hidden_directory_with_new_key(self, tmpdir): 74 | d = tmpdir.mkdir("hidden_directory") 75 | f = d.join(self.files["priv"]) 76 | f.write_binary(self.generate_new_key()) 77 | o = self.onion_cls(hidden_service_path=d, private_key=self.private_key) 78 | with pytest.raises(Exception): 79 | o.write_hidden_service() 80 | o.write_hidden_service(force=True) 81 | assert d.join(self.files["priv"]).read_binary() == o.get_private_key() 82 | assert d.join(self.files["onion"]).read() == o.onion_hostname 83 | 84 | def test_version(self): 85 | assert self.onion_cls().version == str(self.version) 86 | 87 | 88 | class TestOnionV2(_testOnion): 89 | 90 | private_key = """ 91 | -----BEGIN RSA PRIVATE KEY----- 92 | MIICXAIBAAKBgQCsMP4gl6g1Q313miPhb1GnDr56ZxIWGsO2PwHM1infkbhlBakR 93 | 6DGQfpE31L1ZKTUxY0OexKbW088v8qCOfjD9Zk1i80JP4xzfWQcwFZ5yM/0fkhm3 94 | zLXqXdEahvRthmFsS8OWusRs/04U247ryTm4k5S0Ch5OTBuvMLzQ8W0yDwIDAQAB 95 | AoGAAZr3U5B2ZgC6E7phKUHjbf5KMlPxrDkVqAZQWvuIKmhuYqq518vlYmZ7rhyS 96 | o1kqAMrfH4TP1WLmJJlLe+ibRk2aonR4e0GbW4x151wcJdT1V3vdWAsVSzG3+dqX 97 | PiGT//DIe0OPSH6ecI8ftFRLODd6f5iGkF4gsUSTcVzAFgkCQQDTY67dRpOD9Ozw 98 | oYH48xe0B9NQCw7g4NSH85jPurJXnpn6lZ6bcl8x8ioAdgLyomR7fO/dJFYLw6uV 99 | LZLqZsVbAkEA0Iei3QcpsJnYgcQG7l5I26Sq3LwoiGRDFKRI6k0e+en9JQJgA3Ay 100 | tsLpyCHv9jQ762F6AVXFru5DmZX40F6AXQJBAIHoKac8Xx1h4FaEuo4WPkPZ50ey 101 | dANIx/OAhTFrp3vnMPNpDV60K8JS8vLzkx4vJBcrkXDSirqSFhkIN9grLi8CQEO2 102 | l5MQPWBkRKK2pc2Hfj8cdIMi8kJ/1CyCwE6c5l8etR3sbIMRTtZ76nAbXRFkmsRv 103 | La/7Syrnobngsh/vX90CQB+PSSBqiPSsK2yPz6Gsd6OLCQ9sdy2oRwFTasH8sZyl 104 | bhJ3M9WzP/EMkAzyW8mVs1moFp3hRcfQlZHl6g1U9D8= 105 | -----END RSA PRIVATE KEY----- 106 | """.strip().encode() 107 | 108 | public_key = b64decode( 109 | """ 110 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsMP4gl6g1Q313miPhb1GnDr56 111 | ZxIWGsO2PwHM1infkbhlBakR6DGQfpE31L1ZKTUxY0OexKbW088v8qCOfjD9Zk1i 112 | 80JP4xzfWQcwFZ5yM/0fkhm3zLXqXdEahvRthmFsS8OWusRs/04U247ryTm4k5S0 113 | Ch5OTBuvMLzQ8W0yDwIDAQAB 114 | """ 115 | ) 116 | onion_hostname = "wcet3bgkj4purdfx.onion" 117 | onion_cls = OnionV2 118 | files = { 119 | "priv": "private_key", 120 | "onion": "hostname", 121 | } 122 | 123 | version = 2 124 | 125 | 126 | class TestOnionV3(_testOnion): 127 | 128 | private_key = b64decode( 129 | """ 130 | PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAACArobDQYyZAWXei4QZwr++j96H1X/gq14N 131 | wLRZ2O5DXuL0EzYKkdhZSILY85q+kfwZH8z4ceqe7u1F+0pQi/sM 132 | """ 133 | ) 134 | 135 | public_key = b64decode( 136 | """ 137 | PT0gZWQyNTUxOXYxLXB1YmxpYzogdHlwZTAgPT0AAAC9kzftiea/kb+TWlCEVNpfUJLVk+rFIoMG 138 | m9/hW13isA== 139 | """ 140 | ) 141 | 142 | onion_hostname = ( 143 | "xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion" 144 | ) 145 | 146 | onion_cls = OnionV3 147 | files = { 148 | "priv": "hs_ed25519_secret_key", 149 | "pub": "hs_ed25519_public_key", 150 | "onion": "hostname", 151 | } 152 | version = 3 153 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, py38, py39, py310 3 | skip_missing_interpreters = true 4 | isolated_build = true 5 | 6 | ; [testenv] 7 | ; deps= 8 | ; pytest 9 | ; pyfakefs 10 | ; pycryptodome 11 | ; commands=pytest -v 12 | 13 | [testenv] 14 | whitelist_externals = poetry 15 | commands = 16 | poetry install -v 17 | poetry run pytest -v 18 | --------------------------------------------------------------------------------