├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── README.md ├── jetty ├── __init__.py ├── __version__.py ├── cli │ ├── __init__.py │ └── application.py ├── project.py ├── schemas │ └── poetry-schema.json └── util │ ├── __init__.py │ └── dicttools.py ├── poetry.lock ├── pyproject.toml ├── test ├── data │ ├── empty │ │ └── pyproject.toml │ ├── projectA │ │ └── pyproject.toml │ └── projectB │ │ └── pyproject.toml └── test_project.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Distribution / packaging 6 | pip-wheel-metadata/ 7 | *.egg-info/ 8 | 9 | # Unit test / coverage reports 10 | .pytest_cache/ 11 | .tox/ 12 | 13 | # Sphinx documentation 14 | docs/_build 15 | 16 | # pyenv 17 | .python-version 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | dist: xenial 5 | install: 6 | - pip install poetry 7 | - poetry install 8 | script: 9 | - poetry run tox 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ahal/jetty.svg?branch=master)](https://travis-ci.org/ahal/jetty) 2 | [![PyPI version](https://badge.fury.io/py/jetty.svg)](https://badge.fury.io/py/jetty) 3 | 4 | # Jetty 5 | 6 | Jetty is a *very thin* wrapper around [Poetry][0], the dependency and package management tool. 7 | Unlike Poetry, which assumes you are using it with a package, Jetty is singularly focused on 8 | dependency management. Jetty accomplishes three things: 9 | 10 | 1. Removes Poetry's requirement to specify a package name, version and description in 11 | `pyproject.toml`. 12 | 2. Removes all commands that aren't related to dependency management. 13 | 3. Provides a programmatic interface to all of the supported commands. 14 | 15 | The interface is automatically generated from Poetry's command line definitions. So for example, if 16 | you would normally run: 17 | 18 | ```shell 19 | $ poetry install --dry-run 20 | ``` 21 | 22 | The equivalent in code becomes: 23 | 24 | ```python 25 | from jetty import Project 26 | project = Project() 27 | project.install(dry_run=True) 28 | ``` 29 | 30 | Otherwise Jetty shamelessly uses Poetry's logic and commands wholesale. 31 | 32 | ## Installation 33 | 34 | ```shell 35 | $ pip install jetty 36 | ``` 37 | 38 | ## FAQ 39 | 40 | **When should I consider Jetty?** 41 | 42 | There are only a few special circumstances where you might want to consider Jetty: 43 | 44 | A. You want to lock dependencies for Python modules that aren't structured as a package. For 45 | example, maybe you are working in a monorepo where modules can be imported across the repo without 46 | the need for packaging. 47 | 48 | B. You are developing tools that use dependency locking. Jetty's programmatic API is handy for this 49 | scenario. 50 | 51 | 52 | **Why not just use Poetry or Pipenv?** 53 | 54 | Tools like [Poetry][0] and [Pipenv][1] are really cool and useful for managing your Python packages, 55 | but they assume that you are working with a package. If you are working with a simple python script 56 | or creating tooling for a monorepo, their self imposed workflows fall apart very quickly (or they 57 | don't even work at all). 58 | 59 | To be clear, if you *are* working with a Python package and don't need to call these APIs 60 | programmatically, then Jetty doesn't offer you any benefits. Just use Poetry, it is awesome! 61 | 62 | 63 | **Why didn't you wrap pip-tools instead?** 64 | 65 | [Pip-tools][2] is another awesome project, which *does* focus solely on dependency management. But it 66 | still doesn't have a programmatic API, and the UX is a bit less polished than what one might expect 67 | from Poetry or Pipenv. Plus after spending some time looking at both codebases, I saw an easier path 68 | forward for wrapping Poetry. 69 | 70 | 71 | **Aren't you just packaging up Poetry and passing it off as your own?** 72 | 73 | Sort of? Jetty uses Poetry as a dependency without modification, everything is accomplished via 74 | wrapping. In the future, I may attempt to cut out modules that aren't necessary to dependency 75 | management. Depending if this project works out for my use case, I may also attempt to upstream 76 | whatever makes sense. 77 | 78 | Poetry is an awesome project that I whole-heartedly recommend and I make no claims for taking 79 | credit. 80 | 81 | 82 | [0]: https://github.com/sdispater/poetry 83 | [1]: https://github.com/pypa/pipenv 84 | [2]: https://github.com/jazzband/pip-tools 85 | -------------------------------------------------------------------------------- /jetty/__init__.py: -------------------------------------------------------------------------------- 1 | from jetty.project import Project # noqa 2 | -------------------------------------------------------------------------------- /jetty/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.3" 2 | -------------------------------------------------------------------------------- /jetty/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from jetty.cli.application import Application 4 | 5 | 6 | def run(): 7 | Application().run() 8 | 9 | 10 | if __name__ == '__main__': 11 | sys.exit(run()) 12 | -------------------------------------------------------------------------------- /jetty/cli/application.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from cleo import Application as BaseApplication 4 | from cleo.formatters import Formatter 5 | from poetry.console.application import Application as PoetryApplication 6 | from poetry.console.commands import ( 7 | AddCommand, 8 | InstallCommand, 9 | LockCommand, 10 | RemoveCommand, 11 | ShowCommand, 12 | UpdateCommand, 13 | ) 14 | 15 | from jetty.__version__ import __version__ 16 | from jetty.project import JettisonedPoetry 17 | 18 | 19 | class Application(PoetryApplication): 20 | 21 | def __init__(self, path=None): 22 | BaseApplication.__init__(self, "Jetty", __version__) 23 | self._path = path 24 | self._poetry = None 25 | self._skip_io_configuration = False 26 | self._formatter = Formatter(True) 27 | self._formatter.add_style("error", "red", options=["bold"]) 28 | 29 | @property 30 | def poetry(self): 31 | if self._poetry: 32 | return self._poetry 33 | 34 | self._poetry = JettisonedPoetry.create(self._path) 35 | return self._poetry 36 | 37 | def get_default_commands(self): 38 | commands = BaseApplication.get_default_commands(self) 39 | 40 | commands += [ 41 | AddCommand(), 42 | RemoveCommand(), 43 | InstallCommand(), 44 | LockCommand(), 45 | ShowCommand(), 46 | UpdateCommand(), 47 | ] 48 | 49 | return commands 50 | -------------------------------------------------------------------------------- /jetty/project.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from cleo.inputs import ArgvInput 6 | from poetry import json 7 | from poetry.packages import ( 8 | Dependency, 9 | Locker, 10 | ProjectPackage, 11 | ) 12 | from poetry.poetry import Poetry 13 | from poetry.utils._compat import Path 14 | from poetry.utils.toml_file import TomlFile 15 | 16 | from jetty.util.dicttools import merge 17 | 18 | here = Path(__file__).parent.resolve() 19 | 20 | 21 | class Project: 22 | 23 | def __init__(self, path=None): 24 | from jetty.cli import Application 25 | self._application = Application(path) 26 | self._poetry = self._application._poetry 27 | self._application._auto_exit = False 28 | self._application._poetry = self._poetry 29 | 30 | for name, command in self._application.all().items(): 31 | func = self._make_func(name, command) 32 | func.__doc__ = command.__doc__ 33 | setattr(self, name, func.__get__(self)) 34 | 35 | def _make_func(self, name, command): 36 | definition = command.get_definition() 37 | args = [] 38 | kwargs = [] 39 | 40 | for arg in definition.get_arguments() + definition.get_options(): 41 | arg_name = arg.get_name().replace("-", "_") 42 | 43 | if hasattr(arg, 'is_required') and arg.is_required(): 44 | args.append(arg_name) 45 | continue 46 | 47 | arg_default = arg.get_default() 48 | if isinstance(arg_default, str): 49 | arg_default = "\"{}\"".format(arg_default) 50 | kwargs.append("{}={}".format(arg_name, arg_default)) 51 | 52 | args = ", ".join(args) 53 | if args: 54 | args = ", " + args 55 | 56 | kwargs = ", ".join(kwargs) 57 | if kwargs: 58 | kwargs = ", " + kwargs 59 | 60 | exec(""" 61 | def {name}(self{args}{kwargs}): 62 | kwargs = locals().copy() 63 | del kwargs["self"] 64 | cmd = self._build_cmd("{name}", **kwargs) 65 | self._run(["{name}"] + cmd) 66 | """.strip().format(name=name, args=args, kwargs=kwargs)) 67 | return locals()[name] 68 | 69 | def _build_cmd(self, command, **kwargs): 70 | definition = self._application.all()[command].get_definition() 71 | cmd = [] 72 | 73 | for name, value in kwargs.items(): 74 | name = name.replace("_", "-") 75 | 76 | if definition.has_argument(name): 77 | arg = definition.get_argument(name) 78 | if value != arg.get_default(): 79 | cmd.append(value) 80 | 81 | elif definition.has_option(name): 82 | opt = definition.get_option(name) 83 | if value != opt.get_default(): 84 | if isinstance(value, bool): 85 | cmd.append("--{}".format(name)) 86 | else: 87 | cmd.append("--{}={}".format(name, value)) 88 | 89 | return cmd 90 | 91 | def _run(self, cmd): 92 | i = ArgvInput(["poetry -v"] + cmd) 93 | self._application.run(i) 94 | 95 | 96 | class JettisonedPoetry(Poetry): 97 | 98 | @classmethod 99 | def create(cls, path=None): 100 | path = path or os.getcwd() 101 | pyproject_file = Path(path) 102 | 103 | if pyproject_file.name != "pyproject.toml": 104 | pyproject_file = pyproject_file / "pyproject.toml" 105 | 106 | if not pyproject_file.exists(): 107 | raise RuntimeError( 108 | "Jetty could not find a pyproject.toml file in {}".format( 109 | path 110 | ) 111 | ) 112 | 113 | local_config = TomlFile(pyproject_file.as_posix()).read() 114 | tool = local_config.setdefault('tool', {}) 115 | 116 | if 'jetty' not in tool and 'poetry' not in tool: 117 | raise RuntimeError("[tool.jetty] section not found in {}" 118 | .format(pyproject_file.name)) 119 | 120 | local_config = merge(tool.get('jetty', {}), tool.get('poetry', {})) 121 | 122 | # Checking validity 123 | cls.check(local_config) 124 | 125 | # Load package 126 | name = local_config.get('name', pyproject_file.parent.name) 127 | version = local_config.get('version', '0') 128 | package = ProjectPackage(name, version, version) 129 | 130 | if 'dependencies' in local_config: 131 | for name, constraint in local_config['dependencies'].items(): 132 | if name.lower() == 'python': 133 | package.python_versions = constraint 134 | continue 135 | 136 | if isinstance(constraint, list): 137 | for _constraint in constraint: 138 | package.add_dependency(name, _constraint) 139 | 140 | continue 141 | 142 | package.add_dependency(name, constraint) 143 | 144 | if 'dev-dependencies' in local_config: 145 | for name, constraint in local_config['dev-dependencies'].items(): 146 | if isinstance(constraint, list): 147 | for _constraint in constraint: 148 | package.add_dependency(name, _constraint, category='dev') 149 | 150 | continue 151 | 152 | package.add_dependency(name, constraint, category='dev') 153 | 154 | extras = local_config.get("extras", {}) 155 | for extra_name, requirements in extras.items(): 156 | package.extras[extra_name] = [] 157 | 158 | # Checking for dependency 159 | for req in requirements: 160 | req = Dependency(req, "*") 161 | 162 | for dep in package.requires: 163 | if dep.name == req.name: 164 | dep.in_extras.append(extra_name) 165 | package.extras[extra_name].append(dep) 166 | 167 | break 168 | 169 | lock = pyproject_file.parent / "poetry.lock" 170 | locker = Locker(lock, local_config) 171 | return cls(pyproject_file, local_config, package, locker) 172 | 173 | @classmethod 174 | def check(cls, config, strict=False): 175 | json.SCHEMA_DIR = Path(here / "schemas").as_posix() 176 | return Poetry.check(config, strict=strict) 177 | -------------------------------------------------------------------------------- /jetty/schemas/poetry-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "name": "Package", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Package name." 10 | }, 11 | "version": { 12 | "type": "string", 13 | "description": "Package version." 14 | }, 15 | "description": { 16 | "type": "string", 17 | "description": "Short package description." 18 | }, 19 | "keywords": { 20 | "type": "array", 21 | "items": { 22 | "type": "string", 23 | "description": "A tag/keyword that this package relates to." 24 | } 25 | }, 26 | "homepage": { 27 | "type": "string", 28 | "description": "Homepage URL for the project.", 29 | "format": "uri" 30 | }, 31 | "repository": { 32 | "type": "string", 33 | "description": "Repository URL for the project.", 34 | "format": "uri" 35 | }, 36 | "documentation": { 37 | "type": "string", 38 | "description": "Documentation URL for the project.", 39 | "format": "uri" 40 | }, 41 | "license": { 42 | "type": "string", 43 | "description": "License name." 44 | }, 45 | "authors": { 46 | "$ref": "#/definitions/authors" 47 | }, 48 | "readme": { 49 | "type": "string", 50 | "description": "The path to the README file" 51 | }, 52 | "classifiers": { 53 | "type": "array", 54 | "description": "A list of trove classifers." 55 | }, 56 | "packages": { 57 | "type": "array", 58 | "description": "A list of packages to include in the final distribution.", 59 | "items": { 60 | "type": "object", 61 | "description": "Information about where the package resides.", 62 | "additionalProperties": false, 63 | "required": [ 64 | "include" 65 | ], 66 | "properties": { 67 | "include": { 68 | "type": "string", 69 | "description": "What to include in the package." 70 | }, 71 | "from": { 72 | "type": "string", 73 | "description": "Where the source directory of the package resides." 74 | } 75 | } 76 | } 77 | }, 78 | "include": { 79 | "type": "array", 80 | "description": "A list of files and folders to include." 81 | }, 82 | "exclude": { 83 | "type": "array", 84 | "description": "A list of files and folders to exclude." 85 | }, 86 | "dependencies": { 87 | "type": "object", 88 | "description": "This is a hash of package name (keys) and version constraints (values) that are required to run this package.", 89 | "required": [ 90 | "python" 91 | ], 92 | "properties": { 93 | "python": { 94 | "type": "string", 95 | "description": "The Python versions the package is compatible with." 96 | } 97 | }, 98 | "$ref": "#/definitions/dependencies", 99 | "additionalProperties": false 100 | }, 101 | "dev-dependencies": { 102 | "type": "object", 103 | "description": "This is a hash of package name (keys) and version constraints (values) that this package requires for developing it (testing tools and such).", 104 | "$ref": "#/definitions/dependencies", 105 | "additionalProperties": false 106 | }, 107 | "extras": { 108 | "type": "object", 109 | "patternProperties": { 110 | "^[a-zA-Z-_.0-9]+$": { 111 | "type": "array", 112 | "items": { 113 | "type": "string" 114 | } 115 | } 116 | } 117 | }, 118 | "build": { 119 | "type": "string", 120 | "description": "The file used to build extensions." 121 | }, 122 | "source": { 123 | "type": "array", 124 | "description": "A set of additional repositories where packages can be found.", 125 | "additionalProperties": { 126 | "$ref": "#/definitions/repository" 127 | }, 128 | "items": { 129 | "$ref": "#/definitions/repository" 130 | } 131 | }, 132 | "scripts": { 133 | "type": "object", 134 | "description": "A hash of scripts to be installed.", 135 | "items": { 136 | "type": "string" 137 | } 138 | }, 139 | "plugins": { 140 | "type": "object", 141 | "description": "A hash of hashes representing plugins", 142 | "patternProperties": { 143 | "^[a-zA-Z-_.0-9]+$": { 144 | "type": "object", 145 | "patternProperties": { 146 | "^[a-zA-Z-_.0-9]+$": { 147 | "type": "string" 148 | } 149 | } 150 | } 151 | } 152 | } 153 | }, 154 | "definitions": { 155 | "authors": { 156 | "type": "array", 157 | "description": "List of authors that contributed to the package. This is typically the main maintainers, not the full list.", 158 | "items": { 159 | "type": "string" 160 | } 161 | }, 162 | "dependencies": { 163 | "type": "object", 164 | "patternProperties": { 165 | "^[a-zA-Z-_.0-9]+$": { 166 | "oneOf": [ 167 | { 168 | "$ref": "#/definitions/dependency" 169 | }, 170 | { 171 | "$ref": "#/definitions/long-dependency" 172 | }, 173 | { 174 | "$ref": "#/definitions/git-dependency" 175 | }, 176 | { 177 | "$ref": "#/definitions/file-dependency" 178 | }, 179 | { 180 | "$ref": "#/definitions/path-dependency" 181 | }, 182 | { 183 | "$ref": "#/definitions/multiple-constraints-dependency" 184 | } 185 | ] 186 | } 187 | } 188 | }, 189 | "dependency": { 190 | "type": "string", 191 | "description": "The constraint of the dependency." 192 | }, 193 | "long-dependency": { 194 | "type": "object", 195 | "required": [ 196 | "version" 197 | ], 198 | "additionalProperties": false, 199 | "properties": { 200 | "version": { 201 | "type": "string", 202 | "description": "The constraint of the dependency." 203 | }, 204 | "python": { 205 | "type": "string", 206 | "description": "The python versions for which the dependency should be installed." 207 | }, 208 | "platform": { 209 | "type": "string", 210 | "description": "The platform(s) for which the dependency should be installed." 211 | }, 212 | "allows-prereleases": { 213 | "type": "boolean", 214 | "description": "Whether the dependency allows prereleases or not." 215 | }, 216 | "optional": { 217 | "type": "boolean", 218 | "description": "Whether the dependency is optional or not." 219 | }, 220 | "extras": { 221 | "type": "array", 222 | "description": "The required extras for this dependency.", 223 | "items": { 224 | "type": "string" 225 | } 226 | } 227 | } 228 | }, 229 | "git-dependency": { 230 | "type": "object", 231 | "required": [ 232 | "git" 233 | ], 234 | "additionalProperties": false, 235 | "properties": { 236 | "git": { 237 | "type": "string", 238 | "description": "The url of the git repository.", 239 | "format": "uri" 240 | }, 241 | "branch": { 242 | "type": "string", 243 | "description": "The branch to checkout." 244 | }, 245 | "tag": { 246 | "type": "string", 247 | "description": "The tag to checkout." 248 | }, 249 | "rev": { 250 | "type": "string", 251 | "description": "The revision to checkout." 252 | }, 253 | "python": { 254 | "type": "string", 255 | "description": "The python versions for which the dependency should be installed." 256 | }, 257 | "platform": { 258 | "type": "string", 259 | "description": "The platform(s) for which the dependency should be installed." 260 | }, 261 | "allows-prereleases": { 262 | "type": "boolean", 263 | "description": "Whether the dependency allows prereleases or not." 264 | }, 265 | "optional": { 266 | "type": "boolean", 267 | "description": "Whether the dependency is optional or not." 268 | }, 269 | "extras": { 270 | "type": "array", 271 | "description": "The required extras for this dependency.", 272 | "items": { 273 | "type": "string" 274 | } 275 | } 276 | } 277 | }, 278 | "file-dependency": { 279 | "type": "object", 280 | "required": [ 281 | "file" 282 | ], 283 | "additionalProperties": false, 284 | "properties": { 285 | "file": { 286 | "type": "string", 287 | "description": "The path to the file." 288 | }, 289 | "python": { 290 | "type": "string", 291 | "description": "The python versions for which the dependency should be installed." 292 | }, 293 | "platform": { 294 | "type": "string", 295 | "description": "The platform(s) for which the dependency should be installed." 296 | }, 297 | "optional": { 298 | "type": "boolean", 299 | "description": "Whether the dependency is optional or not." 300 | }, 301 | "extras": { 302 | "type": "array", 303 | "description": "The required extras for this dependency.", 304 | "items": { 305 | "type": "string" 306 | } 307 | } 308 | } 309 | }, 310 | "path-dependency": { 311 | "type": "object", 312 | "required": [ 313 | "path" 314 | ], 315 | "additionalProperties": false, 316 | "properties": { 317 | "path": { 318 | "type": "string", 319 | "description": "The path to the dependency." 320 | }, 321 | "python": { 322 | "type": "string", 323 | "description": "The python versions for which the dependency should be installed." 324 | }, 325 | "platform": { 326 | "type": "string", 327 | "description": "The platform(s) for which the dependency should be installed." 328 | }, 329 | "optional": { 330 | "type": "boolean", 331 | "description": "Whether the dependency is optional or not." 332 | }, 333 | "extras": { 334 | "type": "array", 335 | "description": "The required extras for this dependency.", 336 | "items": { 337 | "type": "string" 338 | } 339 | }, 340 | "develop": { 341 | "type": "boolean", 342 | "description": "Whether to install the dependency in development mode." 343 | } 344 | } 345 | }, 346 | "multiple-constraints-dependency": { 347 | "type": "array", 348 | "minItems": 1, 349 | "items": { 350 | "oneOf": [ 351 | { 352 | "$ref": "#/definitions/dependency" 353 | }, 354 | { 355 | "$ref": "#/definitions/long-dependency" 356 | }, 357 | { 358 | "$ref": "#/definitions/git-dependency" 359 | }, 360 | { 361 | "$ref": "#/definitions/file-dependency" 362 | }, 363 | { 364 | "$ref": "#/definitions/path-dependency" 365 | } 366 | ] 367 | } 368 | }, 369 | "scripts": { 370 | "type": "object", 371 | "patternProperties": { 372 | "^[a-zA-Z-_.0-9]+$": { 373 | "oneOf": [ 374 | { 375 | "$ref": "#/definitions/script" 376 | }, 377 | { 378 | "$ref": "#/definitions/extra-script" 379 | } 380 | ] 381 | } 382 | } 383 | }, 384 | "script": { 385 | "type": "string", 386 | "description": "A simple script pointing to a callable object." 387 | }, 388 | "extra-script": { 389 | "type": "object", 390 | "description": "A script that should be installed only if extras are activated.", 391 | "properties": { 392 | "callable": { 393 | "$ref": "#/definitions/script" 394 | }, 395 | "extras": { 396 | "type": "array", 397 | "description": "The required extras for this script.", 398 | "items": { 399 | "type": "string" 400 | } 401 | } 402 | } 403 | }, 404 | "repository": { 405 | "type": "object", 406 | "properties": { 407 | "name": { 408 | "type": "string", 409 | "description": "The name of the repository" 410 | }, 411 | "url": { 412 | "type": "string", 413 | "description": "The url of the repository", 414 | "format": "uri" 415 | } 416 | } 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /jetty/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahal/jetty/53fa3cf9ade65e19c7eebc63dae84e8443077f22/jetty/util/__init__.py -------------------------------------------------------------------------------- /jetty/util/dicttools.py: -------------------------------------------------------------------------------- 1 | def merge(source, dest): 2 | """ 3 | Merge dict and arrays (override scalar values). 4 | 5 | Keys from source override keys from dest, and elements from lists in source 6 | are appended to lists in dest. 7 | 8 | Args: 9 | source (dict): to copy from 10 | dest (dict): to copy to (modified in place) 11 | """ 12 | for key, value in source.items(): 13 | 14 | if key not in dest: 15 | dest[key] = value 16 | continue 17 | 18 | # Merge dict 19 | if isinstance(value, dict) and isinstance(dest[key], dict): 20 | merge(value, dest[key]) 21 | continue 22 | 23 | if isinstance(value, list) and isinstance(dest[key], list): 24 | dest[key] = dest[key] + value 25 | continue 26 | 27 | dest[key] = value 28 | 29 | return dest 30 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "Atomic file writes." 4 | name = "atomicwrites" 5 | optional = false 6 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 7 | version = "1.3.0" 8 | 9 | [[package]] 10 | category = "main" 11 | description = "Classes Without Boilerplate" 12 | name = "attrs" 13 | optional = false 14 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 15 | version = "19.1.0" 16 | 17 | [[package]] 18 | category = "main" 19 | description = "httplib2 caching for requests" 20 | name = "cachecontrol" 21 | optional = false 22 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 23 | version = "0.12.5" 24 | 25 | [package.dependencies] 26 | msgpack = "*" 27 | requests = "*" 28 | 29 | [[package]] 30 | category = "main" 31 | description = "Cachy provides a simple yet effective caching library." 32 | name = "cachy" 33 | optional = false 34 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 35 | version = "0.2.0" 36 | 37 | [[package]] 38 | category = "main" 39 | description = "Python package for providing Mozilla's CA Bundle." 40 | name = "certifi" 41 | optional = false 42 | python-versions = "*" 43 | version = "2019.3.9" 44 | 45 | [[package]] 46 | category = "main" 47 | description = "Universal encoding detector for Python 2 and 3" 48 | name = "chardet" 49 | optional = false 50 | python-versions = "*" 51 | version = "3.0.4" 52 | 53 | [[package]] 54 | category = "main" 55 | description = "Cleo allows you to create beautiful and testable command-line interfaces." 56 | name = "cleo" 57 | optional = false 58 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 59 | version = "0.6.8" 60 | 61 | [package.dependencies] 62 | pastel = ">=0.1.0,<0.2.0" 63 | pylev = ">=1.3,<2.0" 64 | 65 | [[package]] 66 | category = "dev" 67 | description = "Cross-platform colored terminal text." 68 | marker = "sys_platform == \"win32\" and python_version != \"3.4\" or sys_platform == \"win32\" and python_version == \"3.4\"" 69 | name = "colorama" 70 | optional = false 71 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 72 | version = "0.4.1" 73 | 74 | [[package]] 75 | category = "dev" 76 | description = "Updated configparser from Python 3.7 for Python 2.6+." 77 | marker = "python_version < \"3.2\"" 78 | name = "configparser" 79 | optional = false 80 | python-versions = ">=2.6" 81 | version = "3.7.4" 82 | 83 | [[package]] 84 | category = "dev" 85 | description = "Backports and enhancements for the contextlib module" 86 | marker = "python_version < \"3\"" 87 | name = "contextlib2" 88 | optional = false 89 | python-versions = "*" 90 | version = "0.5.5" 91 | 92 | [[package]] 93 | category = "dev" 94 | description = "Discover and load entry points from installed packages." 95 | name = "entrypoints" 96 | optional = false 97 | python-versions = ">=2.7" 98 | version = "0.3" 99 | 100 | [package.dependencies] 101 | [package.dependencies.configparser] 102 | python = ">=2.7,<2.8" 103 | version = ">=3.5" 104 | 105 | [[package]] 106 | category = "main" 107 | description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" 108 | marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.4\"" 109 | name = "enum34" 110 | optional = false 111 | python-versions = "*" 112 | version = "1.1.6" 113 | 114 | [[package]] 115 | category = "dev" 116 | description = "A platform independent file lock." 117 | name = "filelock" 118 | optional = false 119 | python-versions = "*" 120 | version = "3.0.12" 121 | 122 | [[package]] 123 | category = "dev" 124 | description = "the modular source code checker: pep8, pyflakes and co" 125 | name = "flake8" 126 | optional = false 127 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 128 | version = "3.7.9" 129 | 130 | [package.dependencies] 131 | entrypoints = ">=0.3.0,<0.4.0" 132 | mccabe = ">=0.6.0,<0.7.0" 133 | pycodestyle = ">=2.5.0,<2.6.0" 134 | pyflakes = ">=2.1.0,<2.2.0" 135 | 136 | [package.dependencies.configparser] 137 | python = "<3.2" 138 | version = "*" 139 | 140 | [package.dependencies.enum34] 141 | python = "<3.4" 142 | version = "*" 143 | 144 | [package.dependencies.functools32] 145 | python = "<3.2" 146 | version = "*" 147 | 148 | [package.dependencies.typing] 149 | python = "<3.5" 150 | version = "*" 151 | 152 | [[package]] 153 | category = "dev" 154 | description = "Flake8 and pylama plugin that checks the ordering of import statements." 155 | name = "flake8-import-order" 156 | optional = false 157 | python-versions = "*" 158 | version = "0.18.1" 159 | 160 | [package.dependencies] 161 | pycodestyle = "*" 162 | setuptools = "*" 163 | 164 | [package.dependencies.enum34] 165 | python = "<2.8" 166 | version = "*" 167 | 168 | [[package]] 169 | category = "dev" 170 | description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" 171 | marker = "python_version < \"3.0\"" 172 | name = "funcsigs" 173 | optional = false 174 | python-versions = "*" 175 | version = "1.0.2" 176 | 177 | [[package]] 178 | category = "main" 179 | description = "Backport of the functools module from Python 3.2.3 for use on 2.7 and PyPy." 180 | marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.2\"" 181 | name = "functools32" 182 | optional = false 183 | python-versions = "*" 184 | version = "3.2.3-2" 185 | 186 | [[package]] 187 | category = "main" 188 | description = "Version of the glob module that can capture patterns and supports recursive wildcards" 189 | marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\"" 190 | name = "glob2" 191 | optional = false 192 | python-versions = "*" 193 | version = "0.6" 194 | 195 | [[package]] 196 | category = "main" 197 | description = "HTML parser based on the WHATWG HTML specification" 198 | name = "html5lib" 199 | optional = false 200 | python-versions = "*" 201 | version = "1.0.1" 202 | 203 | [package.dependencies] 204 | six = ">=1.9" 205 | webencodings = "*" 206 | 207 | [[package]] 208 | category = "main" 209 | description = "Internationalized Domain Names in Applications (IDNA)" 210 | name = "idna" 211 | optional = false 212 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 213 | version = "2.8" 214 | 215 | [[package]] 216 | category = "dev" 217 | description = "Read metadata from Python packages" 218 | marker = "python_version < \"3.8\"" 219 | name = "importlib-metadata" 220 | optional = false 221 | python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" 222 | version = "0.22" 223 | 224 | [package.dependencies] 225 | zipp = ">=0.5" 226 | 227 | [package.dependencies.configparser] 228 | python = "<3" 229 | version = ">=3.5" 230 | 231 | [package.dependencies.contextlib2] 232 | python = "<3" 233 | version = "*" 234 | 235 | [[package]] 236 | category = "main" 237 | description = "An implementation of JSON Schema validation for Python" 238 | name = "jsonschema" 239 | optional = false 240 | python-versions = "*" 241 | version = "3.0.1" 242 | 243 | [package.dependencies] 244 | attrs = ">=17.4.0" 245 | pyrsistent = ">=0.14.0" 246 | setuptools = "*" 247 | six = ">=1.11.0" 248 | 249 | [package.dependencies.functools32] 250 | python = "<3" 251 | version = "*" 252 | 253 | [[package]] 254 | category = "dev" 255 | description = "McCabe checker, plugin for flake8" 256 | name = "mccabe" 257 | optional = false 258 | python-versions = "*" 259 | version = "0.6.1" 260 | 261 | [[package]] 262 | category = "dev" 263 | description = "More routines for operating on iterables, beyond itertools" 264 | marker = "python_version < \"3.8\"" 265 | name = "more-itertools" 266 | optional = false 267 | python-versions = "*" 268 | version = "5.0.0" 269 | 270 | [package.dependencies] 271 | six = ">=1.0.0,<2.0.0" 272 | 273 | [[package]] 274 | category = "dev" 275 | description = "More routines for operating on iterables, beyond itertools" 276 | marker = "python_version < \"3.8\" or python_version > \"2.7\"" 277 | name = "more-itertools" 278 | optional = false 279 | python-versions = ">=3.4" 280 | version = "7.0.0" 281 | 282 | [[package]] 283 | category = "main" 284 | description = "MessagePack (de)serializer." 285 | name = "msgpack" 286 | optional = false 287 | python-versions = "*" 288 | version = "0.6.1" 289 | 290 | [[package]] 291 | category = "dev" 292 | description = "Core utilities for Python packages" 293 | name = "packaging" 294 | optional = false 295 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 296 | version = "19.1" 297 | 298 | [package.dependencies] 299 | attrs = "*" 300 | pyparsing = ">=2.0.2" 301 | six = "*" 302 | 303 | [[package]] 304 | category = "main" 305 | description = "Bring colors to your terminal." 306 | name = "pastel" 307 | optional = false 308 | python-versions = "*" 309 | version = "0.1.0" 310 | 311 | [[package]] 312 | category = "main" 313 | description = "Object-oriented filesystem paths" 314 | marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\" or python_version < \"3.6\"" 315 | name = "pathlib2" 316 | optional = false 317 | python-versions = "*" 318 | version = "2.3.3" 319 | 320 | [package.dependencies] 321 | six = "*" 322 | 323 | [package.dependencies.scandir] 324 | python = "<3.5" 325 | version = "*" 326 | 327 | [[package]] 328 | category = "main" 329 | description = "Query metadatdata from sdists / bdists / installed packages." 330 | name = "pkginfo" 331 | optional = false 332 | python-versions = "*" 333 | version = "1.5.0.1" 334 | 335 | [[package]] 336 | category = "dev" 337 | description = "plugin and hook calling mechanisms for python" 338 | name = "pluggy" 339 | optional = false 340 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 341 | version = "0.13.0" 342 | 343 | [package.dependencies] 344 | [package.dependencies.importlib-metadata] 345 | python = "<3.8" 346 | version = ">=0.12" 347 | 348 | [[package]] 349 | category = "main" 350 | description = "Python dependency management and packaging made easy." 351 | name = "poetry" 352 | optional = false 353 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 354 | version = "0.12.17" 355 | 356 | [package.dependencies] 357 | cachecontrol = ">=0.12.4,<0.13.0" 358 | cachy = ">=0.2,<0.3" 359 | cleo = ">=0.6.7,<0.7.0" 360 | html5lib = ">=1.0,<2.0" 361 | jsonschema = ">=3.0a3,<4.0" 362 | pkginfo = ">=1.4,<2.0" 363 | pyparsing = ">=2.2,<3.0" 364 | pyrsistent = ">=0.14.2,<0.15.0" 365 | requests = ">=2.18,<3.0" 366 | requests-toolbelt = ">=0.8.0,<0.9.0" 367 | shellingham = ">=1.1,<2.0" 368 | tomlkit = ">=0.5.1,<0.6.0" 369 | 370 | [package.dependencies.functools32] 371 | python = ">=2.7,<2.8" 372 | version = ">=3.2.3,<4.0.0" 373 | 374 | [package.dependencies.glob2] 375 | python = ">=2.7,<2.8 || >=3.4,<3.5" 376 | version = ">=0.6,<0.7" 377 | 378 | [package.dependencies.pathlib2] 379 | python = ">=2.7,<2.8 || >=3.4,<3.5" 380 | version = ">=2.3,<3.0" 381 | 382 | [package.dependencies.subprocess32] 383 | python = ">=2.7,<2.8 || >=3.4,<3.5" 384 | version = ">=3.5,<4.0" 385 | 386 | [package.dependencies.typing] 387 | python = ">=2.7,<2.8 || >=3.4,<3.5" 388 | version = ">=3.6,<4.0" 389 | 390 | [package.dependencies.virtualenv] 391 | python = ">=2.7,<2.8" 392 | version = ">=16.0,<17.0" 393 | 394 | [[package]] 395 | category = "dev" 396 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 397 | name = "py" 398 | optional = false 399 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 400 | version = "1.8.0" 401 | 402 | [[package]] 403 | category = "dev" 404 | description = "Python style guide checker" 405 | name = "pycodestyle" 406 | optional = false 407 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 408 | version = "2.5.0" 409 | 410 | [[package]] 411 | category = "dev" 412 | description = "passive checker of Python programs" 413 | name = "pyflakes" 414 | optional = false 415 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 416 | version = "2.1.1" 417 | 418 | [[package]] 419 | category = "main" 420 | description = "A pure Python Levenshtein implementation that's not freaking GPL'd." 421 | name = "pylev" 422 | optional = false 423 | python-versions = "*" 424 | version = "1.3.0" 425 | 426 | [[package]] 427 | category = "main" 428 | description = "Python parsing module" 429 | name = "pyparsing" 430 | optional = false 431 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 432 | version = "2.4.0" 433 | 434 | [[package]] 435 | category = "main" 436 | description = "Persistent/Functional/Immutable data structures" 437 | name = "pyrsistent" 438 | optional = false 439 | python-versions = "*" 440 | version = "0.14.11" 441 | 442 | [package.dependencies] 443 | six = "*" 444 | 445 | [[package]] 446 | category = "dev" 447 | description = "pytest: simple powerful testing with Python" 448 | name = "pytest" 449 | optional = false 450 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 451 | version = "4.6.9" 452 | 453 | [package.dependencies] 454 | atomicwrites = ">=1.0" 455 | attrs = ">=17.4.0" 456 | packaging = "*" 457 | pluggy = ">=0.12,<1.0" 458 | py = ">=1.5.0" 459 | six = ">=1.10.0" 460 | wcwidth = "*" 461 | 462 | [[package.dependencies.colorama]] 463 | python = "<3.4.0 || >=3.5.0" 464 | version = "*" 465 | 466 | [[package.dependencies.colorama]] 467 | python = ">=3.4,<3.5" 468 | version = "<=0.4.1" 469 | 470 | [[package.dependencies.more-itertools]] 471 | python = "<2.8" 472 | version = ">=4.0.0,<6.0.0" 473 | 474 | [[package.dependencies.more-itertools]] 475 | python = ">=2.8" 476 | version = ">=4.0.0" 477 | 478 | [package.dependencies.funcsigs] 479 | python = "<3.0" 480 | version = ">=1.0" 481 | 482 | [package.dependencies.importlib-metadata] 483 | python = "<3.8" 484 | version = ">=0.12" 485 | 486 | [package.dependencies.pathlib2] 487 | python = "<3.6" 488 | version = ">=2.2.0" 489 | 490 | [[package]] 491 | category = "main" 492 | description = "Python HTTP for Humans." 493 | name = "requests" 494 | optional = false 495 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 496 | version = "2.21.0" 497 | 498 | [package.dependencies] 499 | certifi = ">=2017.4.17" 500 | chardet = ">=3.0.2,<3.1.0" 501 | idna = ">=2.5,<2.9" 502 | urllib3 = ">=1.21.1,<1.25" 503 | 504 | [[package]] 505 | category = "main" 506 | description = "Python HTTP for Humans." 507 | name = "requests" 508 | optional = false 509 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 510 | version = "2.22.0" 511 | 512 | [package.dependencies] 513 | certifi = ">=2017.4.17" 514 | chardet = ">=3.0.2,<3.1.0" 515 | idna = ">=2.5,<2.9" 516 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 517 | 518 | [[package]] 519 | category = "main" 520 | description = "A utility belt for advanced users of python-requests" 521 | name = "requests-toolbelt" 522 | optional = false 523 | python-versions = "*" 524 | version = "0.8.0" 525 | 526 | [package.dependencies] 527 | requests = ">=2.0.1,<3.0.0" 528 | 529 | [[package]] 530 | category = "main" 531 | description = "scandir, a better directory iterator and faster os.walk()" 532 | marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\" or python_version < \"3.5\"" 533 | name = "scandir" 534 | optional = false 535 | python-versions = "*" 536 | version = "1.10.0" 537 | 538 | [[package]] 539 | category = "main" 540 | description = "Tool to Detect Surrounding Shell" 541 | name = "shellingham" 542 | optional = false 543 | python-versions = ">=2.6,!=3.0,!=3.1,!=3.2,!=3.3" 544 | version = "1.3.1" 545 | 546 | [[package]] 547 | category = "main" 548 | description = "Python 2 and 3 compatibility utilities" 549 | name = "six" 550 | optional = false 551 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 552 | version = "1.12.0" 553 | 554 | [[package]] 555 | category = "main" 556 | description = "A backport of the subprocess module from Python 3 for use on 2.x." 557 | marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\"" 558 | name = "subprocess32" 559 | optional = false 560 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" 561 | version = "3.5.4" 562 | 563 | [[package]] 564 | category = "dev" 565 | description = "Python Library for Tom's Obvious, Minimal Language" 566 | name = "toml" 567 | optional = false 568 | python-versions = "*" 569 | version = "0.10.0" 570 | 571 | [[package]] 572 | category = "main" 573 | description = "Style preserving TOML library" 574 | name = "tomlkit" 575 | optional = false 576 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 577 | version = "0.5.11" 578 | 579 | [package.dependencies] 580 | [package.dependencies.enum34] 581 | python = ">=2.7,<2.8" 582 | version = ">=1.1,<2.0" 583 | 584 | [package.dependencies.functools32] 585 | python = ">=2.7,<2.8" 586 | version = ">=3.2.3,<4.0.0" 587 | 588 | [package.dependencies.typing] 589 | python = ">=2.7,<2.8 || >=3.4,<3.5" 590 | version = ">=3.6,<4.0" 591 | 592 | [[package]] 593 | category = "dev" 594 | description = "tox is a generic virtualenv management and test command line tool" 595 | name = "tox" 596 | optional = false 597 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 598 | version = "3.14.0" 599 | 600 | [package.dependencies] 601 | filelock = ">=3.0.0,<4" 602 | packaging = ">=14" 603 | pluggy = ">=0.12.0,<1" 604 | py = ">=1.4.17,<2" 605 | six = ">=1.0.0,<2" 606 | toml = ">=0.9.4" 607 | virtualenv = ">=14.0.0" 608 | 609 | [package.dependencies.importlib-metadata] 610 | python = "<3.8" 611 | version = ">=0.12,<1" 612 | 613 | [[package]] 614 | category = "main" 615 | description = "Type Hints for Python" 616 | marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\" or python_version < \"3.5\"" 617 | name = "typing" 618 | optional = false 619 | python-versions = "*" 620 | version = "3.6.6" 621 | 622 | [[package]] 623 | category = "main" 624 | description = "HTTP library with thread-safe connection pooling, file post, and more." 625 | name = "urllib3" 626 | optional = false 627 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 628 | version = "1.24.3" 629 | 630 | [[package]] 631 | category = "main" 632 | description = "HTTP library with thread-safe connection pooling, file post, and more." 633 | name = "urllib3" 634 | optional = false 635 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 636 | version = "1.25.3" 637 | 638 | [[package]] 639 | category = "main" 640 | description = "Virtual Python Environment builder" 641 | name = "virtualenv" 642 | optional = false 643 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 644 | version = "16.6.0" 645 | 646 | [[package]] 647 | category = "dev" 648 | description = "Measures number of Terminal column cells of wide-character codes" 649 | name = "wcwidth" 650 | optional = false 651 | python-versions = "*" 652 | version = "0.1.7" 653 | 654 | [[package]] 655 | category = "main" 656 | description = "Character encoding aliases for legacy web content" 657 | name = "webencodings" 658 | optional = false 659 | python-versions = "*" 660 | version = "0.5.1" 661 | 662 | [[package]] 663 | category = "dev" 664 | description = "Backport of pathlib-compatible object wrapper for zip files" 665 | marker = "python_version < \"3.8\"" 666 | name = "zipp" 667 | optional = false 668 | python-versions = ">=2.7" 669 | version = "0.6.0" 670 | 671 | [package.dependencies] 672 | more-itertools = "*" 673 | 674 | [metadata] 675 | content-hash = "5b90784835760d337f1dabe38f8e0f54a8a23219bbb525eab6d79de168703c12" 676 | python-versions = "~2.7 || ^3.4" 677 | 678 | [metadata.hashes] 679 | atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] 680 | attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] 681 | cachecontrol = ["cef77effdf51b43178f6a2d3b787e3734f98ade253fa3187f3bb7315aaa42ff7"] 682 | cachy = ["b71513e5a38ce90c1280c02b7d8d6bb3fdf64666c9cc0584f2479afea097d56c", "b71e8e7ddb5b386e23e81befdfac8a93885406139b8681bedc17b3444fcb8fca"] 683 | certifi = ["59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", "b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"] 684 | chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] 685 | cleo = ["85a63076b72ca376fb06668be1fc7758dc16740b394783d5cc65200c4b32f71b", "9b7f79f1aa470a025c0d28c76aa225ee9e65028d32f80032e871aa3500df61b8"] 686 | colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] 687 | configparser = ["8be81d89d6e7b4c0d4e44bcc525845f6da25821de80cb5e06e7e0238a2899e32", "da60d0014fd8c55eb48c1c5354352e363e2d30bbf7057e5e171a468390184c75"] 688 | contextlib2 = ["509f9419ee91cdd00ba34443217d5ca51f5a364a404e1dce9e8979cea969ca48", "f5260a6e679d2ff42ec91ec5252f4eeffdcf21053db9113bd0a8e4d953769c00"] 689 | entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] 690 | enum34 = ["2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", "644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", "6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", "8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"] 691 | filelock = ["18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", "929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"] 692 | flake8 = ["45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", "49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"] 693 | flake8-import-order = ["90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543", "a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"] 694 | funcsigs = ["330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca", "a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"] 695 | functools32 = ["89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0", "f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d"] 696 | glob2 = ["f5b0a686ff21f820c4d3f0c4edd216704cea59d79d00fa337e244a2f2ff83ed6"] 697 | html5lib = ["20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", "66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"] 698 | idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] 699 | importlib-metadata = ["652234b6ab8f2506ae58e528b6fbcc668831d3cc758e1bc01ef438d328b68cdb", "6f264986fb88042bc1f0535fa9a557e6a376cfe5679dc77caac7fe8b5d43d05f"] 700 | jsonschema = ["0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", "a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a"] 701 | mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] 702 | more-itertools = ["38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", "c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", "fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9", "2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", "c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"] 703 | msgpack = ["26cb40116111c232bc235ce131cc3b4e76549088cb154e66a2eb8ff6fcc907ec", "300fd3f2c664a3bf473d6a952f843b4a71454f4c592ed7e74a36b205c1782d28", "3129c355342853007de4a2a86e75eab966119733eb15748819b6554363d4e85c", "31f6d645ee5a97d59d3263fab9e6be76f69fa131cddc0d94091a3c8aca30d67a", "3ce7ef7ee2546c3903ca8c934d09250531b80c6127e6478781ae31ed835aac4c", "4008c72f5ef2b7936447dcb83db41d97e9791c83221be13d5e19db0796df1972", "62bd8e43d204580308d477a157b78d3fee2fb4c15d32578108dc5d89866036c8", "70cebfe08fb32f83051971264466eadf183101e335d8107b80002e632f425511", "72cb7cf85e9df5251abd7b61a1af1fb77add15f40fa7328e924a9c0b6bc7a533", "7c55649965c35eb32c499d17dadfb8f53358b961582846e1bc06f66b9bccc556", "86b963a5de11336ec26bc4f839327673c9796b398b9f1fe6bb6150c2a5d00f0f", "8c73c9bcdfb526247c5e4f4f6cf581b9bb86b388df82cfcaffde0a6e7bf3b43a", "8e68c76c6aff4849089962d25346d6784d38e02baa23ffa513cf46be72e3a540", "97ac6b867a8f63debc64f44efdc695109d541ecc361ee2dce2c8884ab37360a1", "9d4f546af72aa001241d74a79caec278bcc007b4bcde4099994732e98012c858", "a28e69fe5468c9f5251c7e4e7232286d71b7dfadc74f312006ebe984433e9746", "fd509d4aa95404ce8d86b4e32ce66d5d706fd6646c205e1c2a715d87078683a2"] 704 | packaging = ["a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", "c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"] 705 | pastel = ["3108af417ec0fa6d0a620e676ec4f02c839ca13e10611586e5d2174b46aa0bc3", "d1fee8079534f99f1805a044fef946d23eee6d6a7cd34292c30e6c16be9a80b9"] 706 | pathlib2 = ["25199318e8cc3c25dcb45cbe084cc061051336d5a9ea2a12448d3d8cb748f742", "5887121d7f7df3603bca2f710e7219f3eca0eb69e0b7cc6e0a022e155ac931a7"] 707 | pkginfo = ["7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", "a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"] 708 | pluggy = ["0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", "fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"] 709 | poetry = ["0133cd4edc77e955de8cd91203b6728f8365e2849d7fa7da1bfbc14b6529ab43", "6e535de38df7e6ab46ff8d197f53632b071675287d1477efc7bf4a5c4c63bc3f"] 710 | py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] 711 | pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] 712 | pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] 713 | pylev = ["063910098161199b81e453025653ec53556c1be7165a9b7c50be2f4d57eae1c3", "1d29a87beb45ebe1e821e7a3b10da2b6b2f4c79b43f482c2df1a1f748a6e114e"] 714 | pyparsing = ["1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", "9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"] 715 | pyrsistent = ["3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2"] 716 | pytest = ["19e8f75eac01dd3f211edd465b39efbcbdc8fc5f7866d7dd49fedb30d8adf339", "c77a5f30a90e0ce24db9eaa14ddfd38d4afb5ea159309bdd2dae55b931bc9324"] 717 | requests = ["502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b", "11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] 718 | requests-toolbelt = ["42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", "f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"] 719 | scandir = ["2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e", "2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022", "2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f", "2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f", "4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae", "67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173", "7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4", "8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32", "92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188", "b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d", "cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"] 720 | shellingham = ["77d37a4fd287c1e663006f7ecf1b9deca9ad492d0082587bd813c44eb49e4e62", "985b23bbd1feae47ca6a6365eacd314d93d95a8a16f8f346945074c28fe6f3e0"] 721 | six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] 722 | subprocess32 = ["88e37c1aac5388df41cc8a8456bb49ebffd321a3ad4d70358e3518176de3a56b", "eb2937c80497978d181efa1b839ec2d9622cf9600a039a79d0e108d1f9aec79d"] 723 | toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] 724 | tomlkit = ["4e1bd6c9197d984528f9ff0cc9db667c317d8881288db50db20eeeb0f6b0380b", "f044eda25647882e5ef22b43a1688fb6ab12af2fc50e8456cdfc751c873101cf"] 725 | tox = ["0bc216b6a2e6afe764476b4a07edf2c1dab99ed82bb146a1130b2e828f5bff5e", "c4f6b319c20ba4913dbfe71ebfd14ff95d1853c4231493608182f66e566ecfe1"] 726 | typing = ["4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", "57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", "a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a"] 727 | urllib3 = ["2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", "a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb", "b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", "dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"] 728 | virtualenv = ["99acaf1e35c7ccf9763db9ba2accbca2f4254d61d1912c5ee364f9cc4a8942a0", "fe51cdbf04e5d8152af06c075404745a7419de27495a83f0d72518ad50be3ce8"] 729 | wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] 730 | webencodings = ["a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", "b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"] 731 | zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"] 732 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "jetty" 3 | version = "0.2.3" 4 | description = "" 5 | authors = ["Andrew Halberstadt "] 6 | license = "MPL-2.0" 7 | 8 | [tool.poetry.dependencies] 9 | python = "~2.7 || ^3.4" 10 | poetry = "^0.12.11" 11 | cleo = "^0.6.7" 12 | tomlkit = "^0.5.3" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "^4.6" 16 | flake8 = "^3.7" 17 | tox = "^3.14" 18 | flake8-import-order = "^0.18.1" 19 | 20 | [tool.poetry.scripts] 21 | jetty = "jetty.cli:run" 22 | 23 | [build-system] 24 | requires = ["poetry>=0.12"] 25 | build-backend = "poetry.masonry.api" 26 | -------------------------------------------------------------------------------- /test/data/empty/pyproject.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahal/jetty/53fa3cf9ade65e19c7eebc63dae84e8443077f22/test/data/empty/pyproject.toml -------------------------------------------------------------------------------- /test/data/projectA/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.jetty] 2 | 3 | [tool.jetty.dependencies] 4 | python = "^3.4" 5 | tomlkit = "^0.5.3" 6 | -------------------------------------------------------------------------------- /test/data/projectB/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | 3 | [tool.poetry.dependencies] 4 | python = "^3.4" 5 | tomlkit = "^0.5.3" 6 | -------------------------------------------------------------------------------- /test/test_project.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from jetty.cli.application import Application 6 | from jetty.project import JettisonedPoetry, Project 7 | 8 | here = Path(__file__).parent.resolve() 9 | 10 | 11 | def test_find_pyproject_file(): 12 | with pytest.raises(RuntimeError): 13 | JettisonedPoetry.create(here / 'data') 14 | 15 | with pytest.raises(RuntimeError): 16 | JettisonedPoetry.create(here / 'data' / 'empty' / 'pyproject.toml') 17 | 18 | config_1 = JettisonedPoetry.create(here / 'data' / 'projectA')._local_config 19 | config_2 = JettisonedPoetry.create(here / 'data' / 'projectA' / 'pyproject.toml')._local_config 20 | config_3 = JettisonedPoetry.create(here / 'data' / 'projectB')._local_config 21 | 22 | assert config_1 == config_2 23 | assert config_2 == config_3 24 | 25 | 26 | def test_api_exists(): 27 | app = Application() 28 | project = Project(here / 'data' / 'projectA') 29 | 30 | for command in app.get_default_commands(): 31 | assert hasattr(project, command.name) 32 | assert callable(getattr(project, command.name)) 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist=True 3 | envlist=py27,py37,flake8 4 | 5 | [testenv] 6 | whitelist_externals=pytest 7 | commands=pytest -vv -rfx --tb=short {posargs} 8 | 9 | [testenv:flake8] 10 | whitelist_externals=flake8 11 | commands=flake8 12 | 13 | [pytest] 14 | filterwarnings = 15 | ignore::DeprecationWarning:html5lib 16 | 17 | [flake8] 18 | max-line-length=100 19 | import-order-style=pycharm 20 | application-import-names=jetty 21 | --------------------------------------------------------------------------------