├── .coveragerc ├── .gitignore ├── CHANGELOG.md ├── Makefile ├── README.md ├── dependencies.png ├── poetry.lock ├── pyproject.toml ├── sourcery-rules_dependencies_create.gif ├── sourcery_rules_generator ├── __init__.py ├── __main__.py ├── cli │ ├── __init__.py │ ├── cli.py │ ├── dependencies_cli.py │ ├── expensive_loop_cli.py │ └── voldemort_cli.py ├── dependencies.py ├── expensive_loop.py ├── models.py ├── voldemort.py └── yaml_converter.py ├── tests ├── __init__.py ├── conftest.py ├── end-to-end │ ├── __init__.py │ └── test_cli.py ├── fixtures │ └── dependency-rules │ │ ├── core-imported-only-by-api.yaml │ │ └── sqlalchemy-imported-only-by-2.yaml ├── integration │ ├── __init__.py │ └── test_dependencies_yaml_rules.py ├── test_version.py └── unit │ ├── __init__.py │ ├── test_cli.py │ ├── test_dependencies.py │ ├── test_expensive_loop.py │ └── test_voldemort.py └── voldemort_create.png /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | command_line=-m pytest 3 | source=sourcery_rules_generator 4 | branch=True 5 | 6 | [report] 7 | fail_under=94 8 | 9 | [json] 10 | pretty_print=True -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.egg-info/ 3 | 4 | .mypy_cache 5 | .pytest_cache 6 | 7 | # coverage 8 | htmlcov 9 | .coverage 10 | coverage.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.6.1] - 2023-03-05 11 | 12 | ### Added 13 | 14 | * Help for command groups. 15 | 16 | ### Updated 17 | 18 | * Help for `create` commands: more usage-focused 19 | 20 | ### Fixed 21 | 22 | * README: order of paragraphs 23 | 24 | ## [0.6.0] - 2023-02-16 25 | 26 | ### Added 27 | 28 | * "Expensive Loop" rule template and command 29 | 30 | ## [0.5.0] - 2023-01-29 31 | 32 | ### Added 33 | 34 | * Dependencies rules: Allow multiple importer packages 35 | 36 | ## [0.4.0] - 2023-01-05 37 | 38 | ### Added 39 | 40 | * "voldemort" rule template and command: names to avoid in your code 41 | 42 | ## [0.3.3] - 2022-12-21 43 | 44 | ### Added 45 | 46 | * Tests with tox [GH-1](https://github.com/sourcery-ai/sourcery-rules-generator/issues/1) 47 | * Support Python 3.9 [GH-2](https://github.com/sourcery-ai/sourcery-rules-generator/issues/2) 48 | 49 | ## [0.3.2] - 2022-12-16 50 | 51 | ### Fixed 52 | 53 | * Dependencies rules: Detect only the package and its subpackages. Don't detect imports that are only a text match. [GH-10](https://github.com/sourcery-ai/sourcery-rules-generator/issues/10) 54 | 55 | ## [0.3.1] - 2022-12-15 56 | 57 | ### Added 58 | 59 | * README: Dependencies use cases 60 | 61 | ## [0.3.0] - 2022-12-14 62 | 63 | ### Added 64 | 65 | * `dependencies create` command: More detailed explanation of the "Dependencies" template. 66 | * `dependencies create` command: `--quiet` option 67 | 68 | ## [0.2.1] - 2022-12-14 69 | 70 | ### Fixed 71 | 72 | * Support fully qualified package names incl. dot [GH-7](https://github.com/sourcery-ai/sourcery-rules-generator/issues/7) 73 | 74 | ## [0.2.0] - 2022-12-13 75 | 76 | ### Added 77 | 78 | * Prepare for public release: README, project metadata 79 | 80 | ## [0.1.0] - 2022-12-08 81 | 82 | ### Added 83 | 84 | * Command to generate dependencies rules -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | cov: ## Run coverage for the unit tests and show coverage report. 2 | coverage run && coverage report && coverage html && coverage json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sourcery Rules Generator 2 | 3 | **This is an experimental project. It might become a part of the [Sourcery CLI](https://docs.sourcery.ai/Overview/Products/Command-Line/).** 4 | 5 | Sourcery Rules Generator creates architecture rules for your project. 6 | 7 | The generated rules can be used by Sourcery to review your project's architecture. 8 | 9 | Currently, the project can create dependency rules. 10 | 11 | ## Usage 12 | 13 | You can create Sourcery rules based on a template with the command: 14 | 15 | ``` 16 | sourcery-rules create 17 | ``` 18 | 19 | Supported templates: 20 | 21 | * [dependencies](#create-dependencies-rules) 22 | * [naming / voldemort](#create-voldemort-rules): avoid some names 23 | * naming / name vs type mismatch (coming soon) 24 | * performance / expensive loop 25 | 26 | For example: 27 | 28 | ``` 29 | sourcery-rules dependencies create 30 | ``` 31 | 32 | ![gif sourcery-rules dependencies create](https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/main/sourcery-rules_dependencies_create.gif) 33 | 34 | ### Create Dependencies Rules 35 | 36 | With the dependencies template, you can create rules to check the dependencies: 37 | 38 | * between the packages of your application 39 | * to external packages. 40 | 41 | Let's say your project has an architecture like this: 42 | 43 | ![dependencies overview](https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/main/dependencies.png) 44 | 45 | You can create rules to ensure: 46 | 47 | * no other package imports `api` 48 | * only `api` imports `core` 49 | * only `db` import `SQLAlchemy` 50 | * etc. 51 | 52 | Run the command: 53 | 54 | ``` 55 | sourcery-rules dependencies create 56 | ``` 57 | 58 | You'll be prompted to provide: 59 | 60 | * a package name 61 | * the packages that are allowed to import the package above 62 | 63 | The 1st parameter is the fully qualified name of a package or module. 64 | It can be a package within your project or an external dependency. 65 | 66 | The 2nd parameter is optional. 67 | You have the following possibilities: 68 | 69 | * 0 allowed importer (e.g. for packages like `api`, `cli`). Leave this parameter empty. 70 | * 1 allowed importer. Provide the importer package's fully qualified name. 71 | * Multiple allowed importers. Provide multiple fully qualified package names separated by a comma `,` 72 | 73 | => 74 | 75 | 2 rules will be generated: 76 | 77 | * 1 for `import` statements 78 | * 1 for `from ... import` statements 79 | 80 | Every generated rule allows imports: 81 | 82 | * within the package itself 83 | * in tests 84 | 85 | ## Dependencies Use Cases 86 | 87 | ### Internal Dependencies Between the Packages of a Project 88 | 89 | * [Law of Demeter](https://en.wikipedia.org/wiki/Law_of_Demeter): Packages should talk only to their "direct neighbors". 90 | * A mature package shouldn't depend on a less mature package 91 | * A core package shouldn't depend on a customer-specific package 92 | 93 | Thanks to [w_t_payne](https://news.ycombinator.com/user?id=w_t_payne) and [hbrn](https://news.ycombinator.com/user?id=hbrn) for their input in this [HackerNews discussion](https://news.ycombinator.com/item?id=33999191#34001608) 😃 94 | 95 | ### External Dependencies 96 | 97 | * [Gateway pattern](https://martinfowler.com/articles/gateway-pattern.html): Ensure that only a dedicated package of your software communicates with an external dependency. 98 | * Ensure that a deprecated library isn't used 99 | 100 | This [blog post](https://sourcery.ai/blog/dependency-rules/) shows a 3-step method of defining dependency rules: 101 | 102 | 1. Draw a diagram showing the optimal dependencies between your packages. 103 | 2. Phrase some rules in a human language based on the diagram: Which package should depend on which? 104 | 3. Translate the rules into code with Sourcery Rules Generator. 105 | 106 | ## Create Voldemort Rules 107 | 108 | With a "voldemort" template, you can create rules that ensure that a specific name isn't used in your code. 109 | 110 | For example: 111 | 112 | * The word `annual` shouldn't be used, because the preferred term is `yearly`. 113 | * The word `util` shouldn't be used, because it's overly general. 114 | 115 | You can create a "voldemort" rule with the command: 116 | 117 | ``` 118 | sourcery-rules voldemort create 119 | ``` 120 | 121 | ![screenshot sourcery-rules voldemort create](https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/main/voldemort_create.png) 122 | 123 | You'll be prompted to provide: 124 | 125 | * the name that you want to avoid 126 | 127 | => 128 | 129 | 5 rules will be generated: 130 | 131 | * function names 132 | * function arguments 133 | * class names 134 | * variable declarations 135 | * variable assignments 136 | 137 | ## Expensive Loop 138 | 139 | Loops often cause performance problems. Especially, if they execute expensive operations: talking to external systems, complex calculations. 140 | 141 | ``` 142 | sourcery-rules expensive-loop create 143 | ``` 144 | 145 | You'll be prompted to provide: 146 | 147 | * the fully qualified name of the function that shouldn't be called in loops 148 | 149 | => 150 | 151 | 2 rules will be generated: 152 | 153 | * for `for` loops 154 | * for `while` loops 155 | 156 | ## Using the Generated Rules 157 | 158 | The generated rules can be used by Sourcery to review your project. 159 | If you copy the generated rules into your project's `.sourcery.yaml`, Sourcery will use them automatically. 160 | 161 | All the generated rules have the tag `architecture`. Once you've copied them to your `.sourcery.yaml`, you can run them with: 162 | 163 | ``` 164 | sourcery review --enable architecture . 165 | ``` 166 | -------------------------------------------------------------------------------- /dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/7493fba8c6b0fdc7052088a89476f9b2123e7058/dependencies.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "attrs" 3 | version = "22.2.0" 4 | description = "Classes Without Boilerplate" 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.extras] 10 | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 11 | dev = ["attrs[docs,tests]"] 12 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] 13 | tests = ["attrs[tests-no-zope]", "zope.interface"] 14 | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] 15 | 16 | [[package]] 17 | name = "black" 18 | version = "22.8.0" 19 | description = "The uncompromising code formatter." 20 | category = "dev" 21 | optional = false 22 | python-versions = ">=3.6.2" 23 | 24 | [package.dependencies] 25 | click = ">=8.0.0" 26 | mypy-extensions = ">=0.4.3" 27 | pathspec = ">=0.9.0" 28 | platformdirs = ">=2" 29 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 30 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 31 | 32 | [package.extras] 33 | colorama = ["colorama (>=0.4.3)"] 34 | d = ["aiohttp (>=3.7.4)"] 35 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 36 | uvloop = ["uvloop (>=0.15.2)"] 37 | 38 | [[package]] 39 | name = "cachetools" 40 | version = "5.2.0" 41 | description = "Extensible memoizing collections and decorators" 42 | category = "dev" 43 | optional = false 44 | python-versions = "~=3.7" 45 | 46 | [[package]] 47 | name = "chardet" 48 | version = "5.1.0" 49 | description = "Universal encoding detector for Python 3" 50 | category = "dev" 51 | optional = false 52 | python-versions = ">=3.7" 53 | 54 | [[package]] 55 | name = "click" 56 | version = "8.1.3" 57 | description = "Composable command line interface toolkit" 58 | category = "main" 59 | optional = false 60 | python-versions = ">=3.7" 61 | 62 | [package.dependencies] 63 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 64 | 65 | [[package]] 66 | name = "colorama" 67 | version = "0.4.6" 68 | description = "Cross-platform colored terminal text." 69 | category = "main" 70 | optional = false 71 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 72 | 73 | [[package]] 74 | name = "commonmark" 75 | version = "0.9.1" 76 | description = "Python parser for the CommonMark Markdown spec" 77 | category = "main" 78 | optional = false 79 | python-versions = "*" 80 | 81 | [package.extras] 82 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 83 | 84 | [[package]] 85 | name = "coverage" 86 | version = "6.4.4" 87 | description = "Code coverage measurement for Python" 88 | category = "dev" 89 | optional = false 90 | python-versions = ">=3.7" 91 | 92 | [package.extras] 93 | toml = ["tomli"] 94 | 95 | [[package]] 96 | name = "distlib" 97 | version = "0.3.6" 98 | description = "Distribution utilities" 99 | category = "dev" 100 | optional = false 101 | python-versions = "*" 102 | 103 | [[package]] 104 | name = "exceptiongroup" 105 | version = "1.0.4" 106 | description = "Backport of PEP 654 (exception groups)" 107 | category = "dev" 108 | optional = false 109 | python-versions = ">=3.7" 110 | 111 | [package.extras] 112 | test = ["pytest (>=6)"] 113 | 114 | [[package]] 115 | name = "filelock" 116 | version = "3.8.2" 117 | description = "A platform independent file lock." 118 | category = "dev" 119 | optional = false 120 | python-versions = ">=3.7" 121 | 122 | [package.extras] 123 | docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] 124 | testing = ["covdefaults (>=2.2.2)", "coverage (>=6.5)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] 125 | 126 | [[package]] 127 | name = "flake8" 128 | version = "5.0.4" 129 | description = "the modular source code checker: pep8 pyflakes and co" 130 | category = "dev" 131 | optional = false 132 | python-versions = ">=3.6.1" 133 | 134 | [package.dependencies] 135 | mccabe = ">=0.7.0,<0.8.0" 136 | pycodestyle = ">=2.9.0,<2.10.0" 137 | pyflakes = ">=2.5.0,<2.6.0" 138 | 139 | [[package]] 140 | name = "iniconfig" 141 | version = "1.1.1" 142 | description = "iniconfig: brain-dead simple config-ini parsing" 143 | category = "dev" 144 | optional = false 145 | python-versions = "*" 146 | 147 | [[package]] 148 | name = "isort" 149 | version = "5.10.1" 150 | description = "A Python utility / library to sort Python imports." 151 | category = "dev" 152 | optional = false 153 | python-versions = ">=3.6.1,<4.0" 154 | 155 | [package.extras] 156 | colors = ["colorama (>=0.4.3,<0.5.0)"] 157 | pipfile-deprecated-finder = ["pipreqs", "requirementslib"] 158 | plugins = ["setuptools"] 159 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 160 | 161 | [[package]] 162 | name = "mccabe" 163 | version = "0.7.0" 164 | description = "McCabe checker, plugin for flake8" 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=3.6" 168 | 169 | [[package]] 170 | name = "mypy" 171 | version = "0.971" 172 | description = "Optional static typing for Python" 173 | category = "dev" 174 | optional = false 175 | python-versions = ">=3.6" 176 | 177 | [package.dependencies] 178 | mypy-extensions = ">=0.4.3" 179 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 180 | typing-extensions = ">=3.10" 181 | 182 | [package.extras] 183 | dmypy = ["psutil (>=4.0)"] 184 | python2 = ["typed-ast (>=1.4.0,<2)"] 185 | reports = ["lxml"] 186 | 187 | [[package]] 188 | name = "mypy-extensions" 189 | version = "0.4.3" 190 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 191 | category = "dev" 192 | optional = false 193 | python-versions = "*" 194 | 195 | [[package]] 196 | name = "packaging" 197 | version = "22.0" 198 | description = "Core utilities for Python packages" 199 | category = "dev" 200 | optional = false 201 | python-versions = ">=3.7" 202 | 203 | [[package]] 204 | name = "pathspec" 205 | version = "0.10.3" 206 | description = "Utility library for gitignore style pattern matching of file paths." 207 | category = "dev" 208 | optional = false 209 | python-versions = ">=3.7" 210 | 211 | [[package]] 212 | name = "platformdirs" 213 | version = "2.6.0" 214 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 215 | category = "dev" 216 | optional = false 217 | python-versions = ">=3.7" 218 | 219 | [package.extras] 220 | docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] 221 | test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 222 | 223 | [[package]] 224 | name = "pluggy" 225 | version = "1.0.0" 226 | description = "plugin and hook calling mechanisms for python" 227 | category = "dev" 228 | optional = false 229 | python-versions = ">=3.6" 230 | 231 | [package.extras] 232 | dev = ["pre-commit", "tox"] 233 | testing = ["pytest", "pytest-benchmark"] 234 | 235 | [[package]] 236 | name = "pycodestyle" 237 | version = "2.9.1" 238 | description = "Python style guide checker" 239 | category = "dev" 240 | optional = false 241 | python-versions = ">=3.6" 242 | 243 | [[package]] 244 | name = "pydantic" 245 | version = "1.10.2" 246 | description = "Data validation and settings management using python type hints" 247 | category = "main" 248 | optional = false 249 | python-versions = ">=3.7" 250 | 251 | [package.dependencies] 252 | typing-extensions = ">=4.1.0" 253 | 254 | [package.extras] 255 | dotenv = ["python-dotenv (>=0.10.4)"] 256 | email = ["email-validator (>=1.0.3)"] 257 | 258 | [[package]] 259 | name = "pyflakes" 260 | version = "2.5.0" 261 | description = "passive checker of Python programs" 262 | category = "dev" 263 | optional = false 264 | python-versions = ">=3.6" 265 | 266 | [[package]] 267 | name = "pygments" 268 | version = "2.13.0" 269 | description = "Pygments is a syntax highlighting package written in Python." 270 | category = "main" 271 | optional = false 272 | python-versions = ">=3.6" 273 | 274 | [package.extras] 275 | plugins = ["importlib-metadata"] 276 | 277 | [[package]] 278 | name = "pyproject-api" 279 | version = "1.2.1" 280 | description = "API to interact with the python pyproject.toml based projects" 281 | category = "dev" 282 | optional = false 283 | python-versions = ">=3.7" 284 | 285 | [package.dependencies] 286 | packaging = ">=21.3" 287 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 288 | 289 | [package.extras] 290 | docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] 291 | testing = ["covdefaults (>=2.2.2)", "importlib-metadata (>=5.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "virtualenv (>=20.17)", "wheel (>=0.38.4)"] 292 | 293 | [[package]] 294 | name = "pytest" 295 | version = "7.2.0" 296 | description = "pytest: simple powerful testing with Python" 297 | category = "dev" 298 | optional = false 299 | python-versions = ">=3.7" 300 | 301 | [package.dependencies] 302 | attrs = ">=19.2.0" 303 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 304 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 305 | iniconfig = "*" 306 | packaging = "*" 307 | pluggy = ">=0.12,<2.0" 308 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 309 | 310 | [package.extras] 311 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 312 | 313 | [[package]] 314 | name = "pytest-mock" 315 | version = "3.8.2" 316 | description = "Thin-wrapper around the mock package for easier use with pytest" 317 | category = "dev" 318 | optional = false 319 | python-versions = ">=3.7" 320 | 321 | [package.dependencies] 322 | pytest = ">=5.0" 323 | 324 | [package.extras] 325 | dev = ["pre-commit", "pytest-asyncio", "tox"] 326 | 327 | [[package]] 328 | name = "rich" 329 | version = "12.6.0" 330 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 331 | category = "main" 332 | optional = false 333 | python-versions = ">=3.6.3,<4.0.0" 334 | 335 | [package.dependencies] 336 | commonmark = ">=0.9.0,<0.10.0" 337 | pygments = ">=2.6.0,<3.0.0" 338 | 339 | [package.extras] 340 | jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] 341 | 342 | [[package]] 343 | name = "ruamel-yaml" 344 | version = "0.17.21" 345 | description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" 346 | category = "main" 347 | optional = false 348 | python-versions = ">=3" 349 | 350 | [package.dependencies] 351 | "ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} 352 | 353 | [package.extras] 354 | docs = ["ryd"] 355 | jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] 356 | 357 | [[package]] 358 | name = "ruamel-yaml-clib" 359 | version = "0.2.7" 360 | description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" 361 | category = "main" 362 | optional = false 363 | python-versions = ">=3.5" 364 | 365 | [[package]] 366 | name = "shellingham" 367 | version = "1.5.0" 368 | description = "Tool to Detect Surrounding Shell" 369 | category = "main" 370 | optional = false 371 | python-versions = ">=3.4" 372 | 373 | [[package]] 374 | name = "tomli" 375 | version = "2.0.1" 376 | description = "A lil' TOML parser" 377 | category = "dev" 378 | optional = false 379 | python-versions = ">=3.7" 380 | 381 | [[package]] 382 | name = "tox" 383 | version = "4.0.16" 384 | description = "tox is a generic virtualenv management and test command line tool" 385 | category = "dev" 386 | optional = false 387 | python-versions = ">=3.7" 388 | 389 | [package.dependencies] 390 | cachetools = ">=5.2" 391 | chardet = ">=5.1" 392 | colorama = ">=0.4.6" 393 | filelock = ">=3.8.2" 394 | packaging = ">=22" 395 | platformdirs = ">=2.6" 396 | pluggy = ">=1" 397 | pyproject-api = ">=1.2.1" 398 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 399 | virtualenv = ">=20.17.1" 400 | 401 | [package.extras] 402 | docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-argparse-cli (>=1.10)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx-copybutton (>=0.5.1)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.8)"] 403 | testing = ["build[virtualenv] (>=0.9)", "covdefaults (>=2.2.2)", "devpi-process (>=0.3)", "diff-cover (>=7.3)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.11.1)", "psutil (>=5.9.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-xdist (>=3.1)", "re-assert (>=1.1)", "time-machine (>=2.8.2)"] 404 | 405 | [[package]] 406 | name = "typer" 407 | version = "0.7.0" 408 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 409 | category = "main" 410 | optional = false 411 | python-versions = ">=3.6" 412 | 413 | [package.dependencies] 414 | click = ">=7.1.1,<9.0.0" 415 | colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} 416 | rich = {version = ">=10.11.0,<13.0.0", optional = true, markers = "extra == \"all\""} 417 | shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} 418 | 419 | [package.extras] 420 | all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 421 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] 422 | doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] 423 | test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 424 | 425 | [[package]] 426 | name = "typing-extensions" 427 | version = "4.4.0" 428 | description = "Backported and Experimental Type Hints for Python 3.7+" 429 | category = "main" 430 | optional = false 431 | python-versions = ">=3.7" 432 | 433 | [[package]] 434 | name = "virtualenv" 435 | version = "20.17.1" 436 | description = "Virtual Python Environment builder" 437 | category = "dev" 438 | optional = false 439 | python-versions = ">=3.6" 440 | 441 | [package.dependencies] 442 | distlib = ">=0.3.6,<1" 443 | filelock = ">=3.4.1,<4" 444 | platformdirs = ">=2.4,<3" 445 | 446 | [package.extras] 447 | docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] 448 | testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] 449 | 450 | [metadata] 451 | lock-version = "1.1" 452 | python-versions = "^3.9" 453 | content-hash = "3e2942fbaa6b84953cb84726d18fd77c550d4dab1d336d561a9bfa5f4b5db529" 454 | 455 | [metadata.files] 456 | attrs = [ 457 | {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, 458 | {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, 459 | ] 460 | black = [ 461 | {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, 462 | {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, 463 | {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, 464 | {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, 465 | {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, 466 | {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, 467 | {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, 468 | {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, 469 | {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, 470 | {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, 471 | {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, 472 | {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, 473 | {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, 474 | {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, 475 | {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, 476 | {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, 477 | {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, 478 | {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, 479 | {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, 480 | {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, 481 | {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, 482 | {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, 483 | {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, 484 | ] 485 | cachetools = [ 486 | {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, 487 | {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, 488 | ] 489 | chardet = [ 490 | {file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"}, 491 | {file = "chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"}, 492 | ] 493 | click = [ 494 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 495 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 496 | ] 497 | colorama = [ 498 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 499 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 500 | ] 501 | commonmark = [ 502 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 503 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 504 | ] 505 | coverage = [ 506 | {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, 507 | {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, 508 | {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, 509 | {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, 510 | {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, 511 | {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, 512 | {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, 513 | {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, 514 | {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, 515 | {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, 516 | {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, 517 | {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, 518 | {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, 519 | {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, 520 | {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, 521 | {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, 522 | {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, 523 | {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, 524 | {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, 525 | {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, 526 | {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, 527 | {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, 528 | {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, 529 | {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, 530 | {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, 531 | {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, 532 | {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, 533 | {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, 534 | {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, 535 | {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, 536 | {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, 537 | {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, 538 | {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, 539 | {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, 540 | {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, 541 | {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, 542 | {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, 543 | {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, 544 | {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, 545 | {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, 546 | {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, 547 | {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, 548 | {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, 549 | {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, 550 | {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, 551 | {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, 552 | {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, 553 | {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, 554 | {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, 555 | {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, 556 | ] 557 | distlib = [ 558 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, 559 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, 560 | ] 561 | exceptiongroup = [ 562 | {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, 563 | {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, 564 | ] 565 | filelock = [ 566 | {file = "filelock-3.8.2-py3-none-any.whl", hash = "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c"}, 567 | {file = "filelock-3.8.2.tar.gz", hash = "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2"}, 568 | ] 569 | flake8 = [ 570 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 571 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 572 | ] 573 | iniconfig = [ 574 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 575 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 576 | ] 577 | isort = [ 578 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 579 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 580 | ] 581 | mccabe = [ 582 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 583 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 584 | ] 585 | mypy = [ 586 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, 587 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, 588 | {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, 589 | {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, 590 | {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, 591 | {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, 592 | {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, 593 | {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, 594 | {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, 595 | {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, 596 | {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, 597 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, 598 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, 599 | {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, 600 | {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, 601 | {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, 602 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, 603 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, 604 | {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, 605 | {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, 606 | {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, 607 | {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, 608 | {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, 609 | ] 610 | mypy-extensions = [ 611 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 612 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 613 | ] 614 | packaging = [ 615 | {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, 616 | {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, 617 | ] 618 | pathspec = [ 619 | {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, 620 | {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, 621 | ] 622 | platformdirs = [ 623 | {file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"}, 624 | {file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"}, 625 | ] 626 | pluggy = [ 627 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 628 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 629 | ] 630 | pycodestyle = [ 631 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 632 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 633 | ] 634 | pydantic = [ 635 | {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, 636 | {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, 637 | {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, 638 | {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, 639 | {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, 640 | {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, 641 | {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, 642 | {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, 643 | {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, 644 | {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, 645 | {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, 646 | {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, 647 | {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, 648 | {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, 649 | {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, 650 | {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, 651 | {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, 652 | {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, 653 | {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, 654 | {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, 655 | {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, 656 | {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, 657 | {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, 658 | {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, 659 | {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, 660 | {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, 661 | {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, 662 | {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, 663 | {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, 664 | {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, 665 | {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, 666 | {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, 667 | {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, 668 | {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, 669 | {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, 670 | {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, 671 | ] 672 | pyflakes = [ 673 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 674 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 675 | ] 676 | pygments = [ 677 | {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, 678 | {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, 679 | ] 680 | pyproject-api = [ 681 | {file = "pyproject_api-1.2.1-py3-none-any.whl", hash = "sha256:155d5623453173b7b4e9379a3146ccef2d52335234eb2d03d6ba730e7dad179c"}, 682 | {file = "pyproject_api-1.2.1.tar.gz", hash = "sha256:093c047d192ceadcab7afd6b501276bf2ce44adf41cb9c313234518cddd20818"}, 683 | ] 684 | pytest = [ 685 | {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, 686 | {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, 687 | ] 688 | pytest-mock = [ 689 | {file = "pytest-mock-3.8.2.tar.gz", hash = "sha256:77f03f4554392558700295e05aed0b1096a20d4a60a4f3ddcde58b0c31c8fca2"}, 690 | {file = "pytest_mock-3.8.2-py3-none-any.whl", hash = "sha256:8a9e226d6c0ef09fcf20c94eb3405c388af438a90f3e39687f84166da82d5948"}, 691 | ] 692 | rich = [ 693 | {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, 694 | {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, 695 | ] 696 | ruamel-yaml = [ 697 | {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, 698 | {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, 699 | ] 700 | ruamel-yaml-clib = [ 701 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, 702 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, 703 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, 704 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, 705 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, 706 | {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, 707 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, 708 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, 709 | {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, 710 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, 711 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, 712 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, 713 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, 714 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, 715 | {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, 716 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, 717 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, 718 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, 719 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, 720 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, 721 | {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, 722 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, 723 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, 724 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, 725 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, 726 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, 727 | {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, 728 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, 729 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, 730 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, 731 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, 732 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, 733 | {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, 734 | {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, 735 | ] 736 | shellingham = [ 737 | {file = "shellingham-1.5.0-py2.py3-none-any.whl", hash = "sha256:a8f02ba61b69baaa13facdba62908ca8690a94b8119b69f5ec5873ea85f7391b"}, 738 | {file = "shellingham-1.5.0.tar.gz", hash = "sha256:72fb7f5c63103ca2cb91b23dee0c71fe8ad6fbfd46418ef17dbe40db51592dad"}, 739 | ] 740 | tomli = [ 741 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 742 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 743 | ] 744 | tox = [ 745 | {file = "tox-4.0.16-py3-none-any.whl", hash = "sha256:1ba98d4a67b815403e616cf6ded707aeb0fadff10702928f9b990274c9703e9f"}, 746 | {file = "tox-4.0.16.tar.gz", hash = "sha256:968fc4e27110defdf15972893cb15fe1669f338c8408d8835077462fb07e07fe"}, 747 | ] 748 | typer = [ 749 | {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"}, 750 | {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"}, 751 | ] 752 | typing-extensions = [ 753 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, 754 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, 755 | ] 756 | virtualenv = [ 757 | {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, 758 | {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, 759 | ] 760 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "sourcery-rules-generator" 3 | version = "0.6.1" 4 | description = "Generate architecture rules for Python projects." 5 | license = "MIT" 6 | authors = ["reka "] 7 | readme = "README.md" 8 | repository = "https://github.com/sourcery-ai/sourcery-rules-generator" 9 | keywords = ["architecture", "development"] 10 | classifiers = [ 11 | "Intended Audience :: Developers", 12 | "Topic :: Software Development", 13 | "Topic :: Software Development :: Libraries :: Python Modules" 14 | ] 15 | 16 | [tool.poetry.scripts] 17 | sourcery-rules = "sourcery_rules_generator.cli.cli:app" 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.9" 21 | typer = {extras = ["all"], version = "0.7.0"} 22 | rich = "^12.6.0" 23 | pydantic = "^1.10.2" 24 | ruamel-yaml = "^0.17.21" 25 | 26 | [tool.poetry.dev-dependencies] 27 | pytest = "7.2.0" 28 | pytest-mock = "3.8.2" 29 | coverage = "6.4.4" 30 | black = "22.8.0" 31 | isort = "5.10.1" 32 | mypy = "0.971" 33 | flake8 = "5.0.4" 34 | tox = "^4.0.16" 35 | 36 | [tool.isort] 37 | profile = "black" 38 | 39 | [tool.tox] 40 | legacy_tox_ini = """ 41 | [tox] 42 | isolated_build = true 43 | envlist = py39,py310,py311 44 | 45 | [testenv] 46 | allowlist_externals = poetry 47 | commands_pre = 48 | poetry install --sync 49 | commands = 50 | poetry run pytest tests/ --import-mode importlib 51 | """ 52 | 53 | [build-system] 54 | requires = ["poetry-core>=1.0.0"] 55 | build-backend = "poetry.core.masonry.api" 56 | -------------------------------------------------------------------------------- /sourcery-rules_dependencies_create.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/7493fba8c6b0fdc7052088a89476f9b2123e7058/sourcery-rules_dependencies_create.gif -------------------------------------------------------------------------------- /sourcery_rules_generator/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.6.1" 2 | -------------------------------------------------------------------------------- /sourcery_rules_generator/__main__.py: -------------------------------------------------------------------------------- 1 | from sourcery_rules_generator.cli.cli import app 2 | 3 | app() 4 | -------------------------------------------------------------------------------- /sourcery_rules_generator/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/7493fba8c6b0fdc7052088a89476f9b2123e7058/sourcery_rules_generator/cli/__init__.py -------------------------------------------------------------------------------- /sourcery_rules_generator/cli/cli.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import typer 4 | 5 | from rich.console import Console 6 | 7 | from sourcery_rules_generator.cli import ( 8 | dependencies_cli, 9 | voldemort_cli, 10 | expensive_loop_cli, 11 | ) 12 | from sourcery_rules_generator import __version__ 13 | 14 | app = typer.Typer(rich_markup_mode="markdown") 15 | app.add_typer( 16 | dependencies_cli.app, name="dependencies", help="Detect not allowed imports." 17 | ) 18 | app.add_typer(voldemort_cli.app, name="voldemort", help="Detect deny-listed words.") 19 | app.add_typer( 20 | expensive_loop_cli.app, 21 | name="expensive-loop", 22 | help="Detect expensive calls in loops.", 23 | ) 24 | 25 | 26 | @app.callback(invoke_without_command=True) 27 | def callback( 28 | ctx: typer.Context, 29 | version: bool = typer.Option(False, help="Print the current version."), 30 | ) -> None: 31 | """Sourcery Rules Generator""" 32 | if version: 33 | Console().print(__version__) 34 | return 35 | if ctx.invoked_subcommand is None: 36 | Console().print(ctx.get_help()) 37 | -------------------------------------------------------------------------------- /sourcery_rules_generator/cli/dependencies_cli.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import sys 4 | import typer 5 | from rich.console import Console 6 | from rich.markdown import Markdown 7 | from rich.prompt import Prompt 8 | from rich.syntax import Syntax 9 | 10 | from sourcery_rules_generator import dependencies 11 | 12 | app = typer.Typer(rich_markup_mode="markdown") 13 | 14 | 15 | DESCRIPTION_MARKDOWN = """ 16 | # "Dependencies" Template 17 | 18 | With the "Dependencies" template, 19 | you can generate rules that check which package 📦 is imported where. 20 | 21 | For example: 22 | 23 | * The `api` package shouldn't be imported by any other package. 24 | * The `core` package should be imported only the `api` package. 25 | 26 | ## Parameters for the "Dependencies" Template 27 | 28 | 1. The package that gets imported. Required. 29 | 2. The package(s) that are allowed to import the package above. This parameter can be empty. You can provide multiple fully qualified package names separated by comma `,`. 30 | """ 31 | 32 | 33 | @app.command() 34 | def create( 35 | package_option: str = typer.Option( 36 | None, 37 | "--package", 38 | help="The fully qualified name of the package. Always exactly 1 package.", 39 | ), 40 | caller_option: str = typer.Option( 41 | None, 42 | "--importer", 43 | help="The fully qualified names of the allowed importers separated by comma. This can be 0, 1 or more importers.", 44 | ), 45 | interactive_flag: bool = typer.Option( 46 | True, 47 | "--interactive/--no-interactive", 48 | "--input/--no-input", 49 | help="Switch whether interactive prompts are shown. Use `--no-input` when you call this command from scripts.", 50 | ), 51 | plain: bool = typer.Option(False, help="Print only plain text."), 52 | quiet: bool = typer.Option( 53 | False, 54 | "--quiet", 55 | "-q", 56 | help='Display less info about the "Dependencies" template.', 57 | ), 58 | ): 59 | """Create rules to check the dependencies between packages of a project. 60 | 61 | Create rules to verify that internal and external packages are imported only by the allowed parts of a project. 62 | """ 63 | interactive = sys.stdin.isatty() and interactive_flag 64 | stderr_console = Console(stderr=True) 65 | if interactive and not quiet: 66 | stderr_console.print(Markdown(DESCRIPTION_MARKDOWN)) 67 | stderr_console.rule() 68 | 69 | if interactive: 70 | stderr_console.print(Markdown('## Parameters for the "Dependencies" Template')) 71 | 72 | package = ( 73 | package_option 74 | or interactive 75 | and Prompt.ask("Package (required)", console=stderr_console) 76 | ) 77 | allowed_importer = ( 78 | caller_option 79 | or interactive 80 | and Prompt.ask( 81 | f"Which packages are allowed to import {package}? (Leave this empty if no package is allowed to import {package}.)", 82 | console=stderr_console, 83 | ) 84 | ) 85 | if not package: 86 | _raise_error("No package provided. Can't create dependency rules.") 87 | 88 | result = dependencies.create_yaml_rules(package, allowed_importer) 89 | 90 | stderr_console.rule() 91 | stderr_console.print(Markdown("## Generated YAML Rules")) 92 | if plain: 93 | Console().print(result) 94 | else: 95 | Console().print(Syntax(result, "YAML")) 96 | 97 | 98 | def _raise_error(error_msg: str, code: int = 1) -> None: 99 | stderr_console = Console(stderr=True, style="bold red") 100 | stderr_console.print(error_msg) 101 | raise typer.Exit(code=code) 102 | -------------------------------------------------------------------------------- /sourcery_rules_generator/cli/expensive_loop_cli.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import sys 4 | import typer 5 | from rich.console import Console 6 | from rich.markdown import Markdown 7 | from rich.prompt import Prompt 8 | from rich.syntax import Syntax 9 | 10 | from sourcery_rules_generator import expensive_loop 11 | 12 | app = typer.Typer(rich_markup_mode="markdown") 13 | 14 | 15 | DESCRIPTION_MARKDOWN = """ 16 | # Expensive Loop Template 17 | 18 | With the "Expensive Loop" template, 19 | you can generate rules that ensure that an expensive operation isn't called in a loop. 20 | 21 | For example: 22 | 23 | * Call to an external API. 24 | * Complex calculations. 25 | 26 | ## Parameters for the "Expensive Loop" Template 27 | 28 | 1. The fully qualified name of the expensive function, that you want to avoid in loops. Required. 29 | """ 30 | 31 | 32 | @app.command() 33 | def create( 34 | avoided_function_option: str = typer.Option( 35 | None, 36 | "--avoided-function", 37 | help="The function that should be avoided in loops.", 38 | ), 39 | interactive_flag: bool = typer.Option( 40 | True, 41 | "--interactive/--no-interactive", 42 | "--input/--no-input", 43 | help="Switch whether interactive prompts are shown. Use `--no-input` when you call this command from scripts.", 44 | ), 45 | plain: bool = typer.Option(False, help="Print only plain text."), 46 | quiet: bool = typer.Option( 47 | False, 48 | "--quiet", 49 | "-q", 50 | help='Display less info about the "Expensive Loop" template.', 51 | ), 52 | ): 53 | """Create a rule to detect an expensive call in a loop.""" 54 | interactive = sys.stdin.isatty() and interactive_flag 55 | stderr_console = Console(stderr=True) 56 | if interactive and not quiet: 57 | stderr_console.print(Markdown(DESCRIPTION_MARKDOWN)) 58 | stderr_console.rule() 59 | 60 | if interactive: 61 | stderr_console.print( 62 | Markdown('## Parameters for the "Expensive Loop" Template') 63 | ) 64 | 65 | function_name = ( 66 | avoided_function_option 67 | or interactive 68 | and Prompt.ask( 69 | "The fully qualified name of the expensive function (required)", 70 | console=stderr_console, 71 | ) 72 | ) 73 | if not function_name: 74 | _raise_error("No function name provided. Can't create Expensive Loop rule.") 75 | 76 | result = expensive_loop.create_yaml_rules(function_name) 77 | 78 | stderr_console.rule() 79 | stderr_console.print(Markdown("## Generated YAML Rules")) 80 | if plain: 81 | Console().print(result) 82 | else: 83 | Console().print(Syntax(result, "YAML")) 84 | 85 | 86 | def _raise_error(error_msg: str, code: int = 1) -> None: 87 | stderr_console = Console(stderr=True, style="bold red") 88 | stderr_console.print(error_msg) 89 | raise typer.Exit(code=code) 90 | -------------------------------------------------------------------------------- /sourcery_rules_generator/cli/voldemort_cli.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import sys 4 | import typer 5 | from rich.console import Console 6 | from rich.markdown import Markdown 7 | from rich.prompt import Prompt 8 | from rich.syntax import Syntax 9 | 10 | from sourcery_rules_generator import voldemort 11 | 12 | app = typer.Typer(rich_markup_mode="markdown") 13 | 14 | 15 | DESCRIPTION_MARKDOWN = """ 16 | # "Voldemort" Template 17 | 18 | With the "Voldemort" template, 19 | you can generate rules that ensure that a specific word isn't used in your codebase. 20 | 21 | For example: 22 | 23 | * The word `annual` shouldn't be used, because the preferred term is `yearly`. 24 | * The word `util` shouldn't be used, because it's overly general. 25 | 26 | ## Parameters for the "Voldemort" Template 27 | 28 | 1. The word that should be avoided. Required. 29 | """ 30 | 31 | 32 | @app.command() 33 | def create( 34 | avoided_name_option: str = typer.Option( 35 | None, 36 | "--avoided-name", 37 | help="The name that should be avoided.", 38 | ), 39 | interactive_flag: bool = typer.Option( 40 | True, 41 | "--interactive/--no-interactive", 42 | "--input/--no-input", 43 | help="Switch whether interactive prompts are shown. Use `--no-input` when you call this command from scripts.", 44 | ), 45 | plain: bool = typer.Option(False, help="Print only plain text."), 46 | quiet: bool = typer.Option( 47 | False, 48 | "--quiet", 49 | "-q", 50 | help='Display less info about the "Voldemort" template.', 51 | ), 52 | ): 53 | """Create a rule to flag a deny-listed word.""" 54 | interactive = sys.stdin.isatty() and interactive_flag 55 | stderr_console = Console(stderr=True) 56 | if interactive and not quiet: 57 | stderr_console.print(Markdown(DESCRIPTION_MARKDOWN)) 58 | stderr_console.rule() 59 | 60 | if interactive: 61 | stderr_console.print(Markdown('## Parameters for the "Voldemort" Template')) 62 | 63 | name_to_avoid = ( 64 | avoided_name_option 65 | or interactive 66 | and Prompt.ask("The name to avoid: (required)", console=stderr_console) 67 | ) 68 | if not name_to_avoid: 69 | _raise_error("No name to avoid provided. Can't create voldemort rule.") 70 | 71 | result = voldemort.create_yaml_rules(name_to_avoid) 72 | 73 | stderr_console.rule() 74 | stderr_console.print(Markdown("## Generated YAML Rules")) 75 | if plain: 76 | Console().print(result) 77 | else: 78 | Console().print(Syntax(result, "YAML")) 79 | 80 | 81 | def _raise_error(error_msg: str, code: int = 1) -> None: 82 | stderr_console = Console(stderr=True, style="bold red") 83 | stderr_console.print(error_msg) 84 | raise typer.Exit(code=code) 85 | -------------------------------------------------------------------------------- /sourcery_rules_generator/dependencies.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sourcery_rules_generator import yaml_converter 4 | from sourcery_rules_generator.models import SourceryCustomRule, PathsConfig 5 | 6 | 7 | def create_yaml_rules(package: str, allowed_importer: Optional[str]) -> str: 8 | 9 | custom_rules = create_sourcery_custom_rules(package, allowed_importer) 10 | 11 | rules_dict = {"rules": [rule.dict(exclude_unset=True) for rule in custom_rules]} 12 | return yaml_converter.dumps(rules_dict) 13 | 14 | 15 | def create_sourcery_custom_rules( 16 | package: str, allowed_importer_text: Optional[str] 17 | ) -> str: 18 | # Dots aren't allowed in the rule ID. 19 | package_slug = package.replace(".", "-") 20 | 21 | # The dot in the package's fully qualified name 22 | # needs to be escaped in the regex used for the condition. 23 | package_in_regex = package.replace(".", "\.") 24 | 25 | exclude_paths = [_path_for_package(package), "tests/"] 26 | 27 | if allowed_importer_text: 28 | if "," in allowed_importer_text: 29 | allowed_importers = allowed_importer_text.split(",") 30 | description = _description_for_multiple_importers( 31 | package, allowed_importers 32 | ) 33 | exclude_paths.extend(_path_for_package(impo) for impo in allowed_importers) 34 | else: 35 | description = _description_for_1_importer(package, allowed_importer_text) 36 | exclude_paths.append(_path_for_package(allowed_importer_text)) 37 | else: 38 | description = _description_for_0_importer(package) 39 | 40 | import_rule = SourceryCustomRule( 41 | id=f"dependency-rules-{package_slug}-import", 42 | description=description, 43 | tags=["architecture", "dependencies"], 44 | pattern="import ..., ${module}, ...", 45 | condition=f'module.matches_regex(r"^{package_in_regex}\\b")', 46 | paths=PathsConfig(exclude=exclude_paths), 47 | ) 48 | 49 | from_rule = SourceryCustomRule( 50 | id=f"dependency-rules-{package_slug}-from", 51 | description=description, 52 | tags=["architecture", "dependencies"], 53 | pattern="from ${module} import ...", 54 | condition=f'module.matches_regex(r"^{package_in_regex}\\b")', 55 | paths=PathsConfig(exclude=exclude_paths), 56 | ) 57 | 58 | return (import_rule, from_rule) 59 | 60 | 61 | def _description_for_0_importer(package: str): 62 | return f"Do not import `{package}` in other packages" 63 | 64 | 65 | def _description_for_1_importer(package: str, allowed_importer: str): 66 | return f"Only `{allowed_importer}` should import `{package}`" 67 | 68 | 69 | def _description_for_multiple_importers(package: str, allowed_importers: list[str]): 70 | quoted = [f"`{impo}`" for impo in allowed_importers] 71 | return f"Only {', '.join(quoted)} should import `{package}`" 72 | 73 | 74 | def _path_for_package(package: str) -> str: 75 | return package.replace(".", "/") + "/" 76 | -------------------------------------------------------------------------------- /sourcery_rules_generator/expensive_loop.py: -------------------------------------------------------------------------------- 1 | from sourcery_rules_generator import yaml_converter 2 | from sourcery_rules_generator.models import SourceryCustomRule 3 | 4 | 5 | def create_yaml_rules(function_name: str): 6 | 7 | custom_rules = create_sourcery_custom_rules(function_name) 8 | 9 | rules_dict = {"rules": [rule.dict(exclude_unset=True) for rule in custom_rules]} 10 | return yaml_converter.dumps(rules_dict) 11 | 12 | 13 | def create_sourcery_custom_rules(function_name: str) -> str: 14 | description = f"Don't call `{function_name}()` in loops." 15 | function_slug = function_name.replace(".", "-") 16 | tag = f"no-{function_slug}-in-loops" 17 | 18 | for_rule = SourceryCustomRule( 19 | id=f"no-{function_slug}-for", 20 | description=description, 21 | tags=["performance", tag], 22 | pattern=f""" 23 | for ... in ... : 24 | ... 25 | {function_name}(...) 26 | ... 27 | """, 28 | ) 29 | while_rule = SourceryCustomRule( 30 | id=f"no-{function_slug}-while", 31 | description=description, 32 | tags=["performance", tag], 33 | pattern=f""" 34 | while ... : 35 | ... 36 | {function_name}(...) 37 | ... 38 | """, 39 | ) 40 | 41 | return ( 42 | for_rule, 43 | while_rule, 44 | ) 45 | -------------------------------------------------------------------------------- /sourcery_rules_generator/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List, Optional 3 | 4 | from pydantic import ConstrainedStr, BaseModel, Field 5 | 6 | RULE_ID_REGEX = re.compile("^[A-Za-z][A-Za-z0-9-_/:]*$") 7 | 8 | 9 | class RuleString(ConstrainedStr): 10 | max_length = 88 11 | regex = RULE_ID_REGEX 12 | 13 | 14 | class PathsConfig(BaseModel): 15 | include: Optional[List[str]] 16 | exclude: Optional[List[str]] 17 | 18 | 19 | class SourceryCustomRule(BaseModel): 20 | id: RuleString = Field() 21 | description: str = Field() 22 | pattern: str = Field() 23 | replacement: Optional[str] = None 24 | condition: Optional[str] = None 25 | explanation: Optional[str] = None 26 | paths: Optional[PathsConfig] = None 27 | # tests: tuple[RuleTestConfig, ...] = () 28 | tags: tuple[RuleString, ...] = () 29 | -------------------------------------------------------------------------------- /sourcery_rules_generator/voldemort.py: -------------------------------------------------------------------------------- 1 | from sourcery_rules_generator import yaml_converter 2 | from sourcery_rules_generator.models import SourceryCustomRule 3 | 4 | 5 | def create_yaml_rules(name_to_avoid: str): 6 | 7 | custom_rules = create_sourcery_custom_rules(name_to_avoid) 8 | 9 | rules_dict = {"rules": [rule.dict(exclude_unset=True) for rule in custom_rules]} 10 | return yaml_converter.dumps(rules_dict) 11 | 12 | 13 | def create_sourcery_custom_rules(name_to_avoid: str) -> str: 14 | description = f"Don't use the name {name_to_avoid}" 15 | 16 | function_name_rule = SourceryCustomRule( 17 | id=f"no-{name_to_avoid}-function-name", 18 | description=description, 19 | tags=["naming", f"no-{name_to_avoid}"], 20 | pattern=""" 21 | def ${function_name}(...): 22 | ... 23 | """, 24 | condition=f'function_name.contains("{name_to_avoid}")', 25 | ) 26 | 27 | function_arg_rule = SourceryCustomRule( 28 | id=f"no-{name_to_avoid}-function-arg", 29 | description=description, 30 | tags=["naming", f"no-{name_to_avoid}"], 31 | pattern=""" 32 | def ...(...,${arg_name}: ${type?} = ${default_value?},...): 33 | ... 34 | """, 35 | condition=f'arg_name.contains("{name_to_avoid}")', 36 | ) 37 | 38 | class_name_rule = SourceryCustomRule( 39 | id=f"no-{name_to_avoid}-class-name", 40 | description=description, 41 | tags=["naming", f"no-{name_to_avoid}"], 42 | pattern=""" 43 | class ${class_name}(...): 44 | ... 45 | """, 46 | condition=f'class_name.contains("{name_to_avoid}")', 47 | ) 48 | 49 | variable_declaration_rule = SourceryCustomRule( 50 | id=f"no-{name_to_avoid}-property", 51 | description=description, 52 | tags=["naming", f"no-{name_to_avoid}"], 53 | pattern="${var}: ${type}", 54 | condition=f'var.contains("{name_to_avoid}")', 55 | ) 56 | 57 | variable_assignment_rule = SourceryCustomRule( 58 | id=f"no-{name_to_avoid}-variable", 59 | description=description, 60 | tags=["naming", f"no-{name_to_avoid}"], 61 | pattern="${var} = ${value}", 62 | condition=f'var.contains("{name_to_avoid}")', 63 | ) 64 | 65 | return ( 66 | function_name_rule, 67 | function_arg_rule, 68 | class_name_rule, 69 | variable_declaration_rule, 70 | variable_assignment_rule, 71 | ) 72 | -------------------------------------------------------------------------------- /sourcery_rules_generator/yaml_converter.py: -------------------------------------------------------------------------------- 1 | from ruamel.yaml import YAML 2 | from ruamel.yaml.compat import StringIO 3 | 4 | 5 | class MyYAML(YAML): 6 | def dumps(self, data, stream=None, **kw): 7 | inefficient = False 8 | if stream is None: 9 | inefficient = True 10 | stream = StringIO() 11 | YAML.dump(self, data, stream, **kw) 12 | if inefficient: 13 | return stream.getvalue() 14 | 15 | 16 | def dumps(obj) -> str: 17 | yaml = MyYAML() # or typ='safe'/'unsafe' etc 18 | return yaml.dumps(obj) 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/7493fba8c6b0fdc7052088a89476f9b2123e7058/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def yaml_rules_only_api_imports_core(): 8 | return Path( 9 | os.path.dirname(__file__), 10 | "fixtures/dependency-rules/core-imported-only-by-api.yaml", 11 | ).read_text() 12 | 13 | 14 | @pytest.fixture 15 | def yaml_rules_only_db_and_db_util_import_sqlalchemy(): 16 | return Path( 17 | os.path.dirname(__file__), 18 | "fixtures/dependency-rules/sqlalchemy-imported-only-by-2.yaml", 19 | ).read_text() 20 | -------------------------------------------------------------------------------- /tests/end-to-end/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/7493fba8c6b0fdc7052088a89476f9b2123e7058/tests/end-to-end/__init__.py -------------------------------------------------------------------------------- /tests/end-to-end/test_cli.py: -------------------------------------------------------------------------------- 1 | from typer.testing import CliRunner 2 | 3 | from sourcery_rules_generator.cli.cli import app 4 | 5 | runner = CliRunner(mix_stderr=False) 6 | 7 | 8 | def test_create_dependency_rule_with_1_importer(yaml_rules_only_api_imports_core): 9 | result = runner.invoke( 10 | app, 11 | ["dependencies", "create", "--package", "core", "--importer", "api", "--plain"], 12 | ) 13 | 14 | assert result.exit_code == 0 15 | assert "Generated YAML Rules" in result.stderr 16 | assert result.stdout == yaml_rules_only_api_imports_core 17 | 18 | 19 | def test_create_dependency_rule_with_2_importers( 20 | yaml_rules_only_db_and_db_util_import_sqlalchemy, 21 | ): 22 | result = runner.invoke( 23 | app, 24 | [ 25 | "dependencies", 26 | "create", 27 | "--package", 28 | "sqlalchemy", 29 | "--importer", 30 | "app.db,app.db_util", 31 | "--plain", 32 | ], 33 | ) 34 | 35 | assert result.exit_code == 0 36 | assert "Generated YAML Rules" in result.stderr 37 | assert result.stdout == yaml_rules_only_db_and_db_util_import_sqlalchemy 38 | -------------------------------------------------------------------------------- /tests/fixtures/dependency-rules/core-imported-only-by-api.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: dependency-rules-core-import 3 | description: Only `api` should import `core` 4 | pattern: import ..., ${module}, ... 5 | condition: module.matches_regex(r"^core\b") 6 | paths: 7 | exclude: 8 | - core/ 9 | - tests/ 10 | - api/ 11 | tags: 12 | - architecture 13 | - dependencies 14 | - id: dependency-rules-core-from 15 | description: Only `api` should import `core` 16 | pattern: from ${module} import ... 17 | condition: module.matches_regex(r"^core\b") 18 | paths: 19 | exclude: 20 | - core/ 21 | - tests/ 22 | - api/ 23 | tags: 24 | - architecture 25 | - dependencies 26 | 27 | -------------------------------------------------------------------------------- /tests/fixtures/dependency-rules/sqlalchemy-imported-only-by-2.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: dependency-rules-sqlalchemy-import 3 | description: Only `app.db`, `app.db_util` should import `sqlalchemy` 4 | pattern: import ..., ${module}, ... 5 | condition: module.matches_regex(r"^sqlalchemy\b") 6 | paths: 7 | exclude: 8 | - sqlalchemy/ 9 | - tests/ 10 | - app/db/ 11 | - app/db_util/ 12 | tags: 13 | - architecture 14 | - dependencies 15 | - id: dependency-rules-sqlalchemy-from 16 | description: Only `app.db`, `app.db_util` should import `sqlalchemy` 17 | pattern: from ${module} import ... 18 | condition: module.matches_regex(r"^sqlalchemy\b") 19 | paths: 20 | exclude: 21 | - sqlalchemy/ 22 | - tests/ 23 | - app/db/ 24 | - app/db_util/ 25 | tags: 26 | - architecture 27 | - dependencies 28 | 29 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/7493fba8c6b0fdc7052088a89476f9b2123e7058/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/test_dependencies_yaml_rules.py: -------------------------------------------------------------------------------- 1 | from sourcery_rules_generator import dependencies 2 | 3 | 4 | def test_create_yaml_rules(): 5 | result = dependencies.create_yaml_rules("core", "api") 6 | 7 | assert result.count("id:") == 2 8 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | from sourcery_rules_generator import __version__ 2 | 3 | 4 | def test_version(): 5 | assert __version__ == "0.6.1" 6 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/7493fba8c6b0fdc7052088a89476f9b2123e7058/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_cli.py: -------------------------------------------------------------------------------- 1 | from typer.testing import CliRunner 2 | 3 | from sourcery_rules_generator.cli.cli import app 4 | 5 | runner = CliRunner(mix_stderr=False) 6 | 7 | 8 | def test_create_dependency_rule_quiet(): 9 | result = runner.invoke( 10 | app, ["dependencies", "create", "--quiet", "--package", "example"] 11 | ) 12 | 13 | assert result.exit_code == 0 14 | assert '"Dependencies" Template' not in result.stderr 15 | assert "Generated YAML Rules" in result.stderr 16 | 17 | 18 | def test_create_dependency_rule_missing_package(): 19 | result = runner.invoke(app, ["dependencies", "create"]) 20 | 21 | assert result.exit_code == 1 22 | assert "No package provided." in result.stderr 23 | -------------------------------------------------------------------------------- /tests/unit/test_dependencies.py: -------------------------------------------------------------------------------- 1 | from sourcery_rules_generator import dependencies 2 | from sourcery_rules_generator.models import SourceryCustomRule, PathsConfig 3 | 4 | 5 | def test_1_allowed_importer(): 6 | result = dependencies.create_sourcery_custom_rules("core", "api") 7 | 8 | expected = ( 9 | SourceryCustomRule( 10 | id="dependency-rules-core-import", 11 | description="Only `api` should import `core`", 12 | tags=["architecture", "dependencies"], 13 | pattern="import ..., ${module}, ...", 14 | condition='module.matches_regex(r"^core\\b")', 15 | paths=PathsConfig(exclude=["core/", "tests/", "api/"]), 16 | ), 17 | SourceryCustomRule( 18 | id="dependency-rules-core-from", 19 | description="Only `api` should import `core`", 20 | tags=["architecture", "dependencies"], 21 | pattern="from ${module} import ...", 22 | condition='module.matches_regex(r"^core\\b")', 23 | paths=PathsConfig(exclude=["core/", "tests/", "api/"]), 24 | ), 25 | ) 26 | 27 | assert expected == result 28 | 29 | 30 | def test_0_allowed_importer(): 31 | result = dependencies.create_sourcery_custom_rules("api", "") 32 | 33 | expected = ( 34 | SourceryCustomRule( 35 | id="dependency-rules-api-import", 36 | description="Do not import `api` in other packages", 37 | tags=["architecture", "dependencies"], 38 | pattern="import ..., ${module}, ...", 39 | condition='module.matches_regex(r"^api\\b")', 40 | paths=PathsConfig(exclude=["api/", "tests/"]), 41 | ), 42 | SourceryCustomRule( 43 | id="dependency-rules-api-from", 44 | description="Do not import `api` in other packages", 45 | tags=["architecture", "dependencies"], 46 | pattern="from ${module} import ...", 47 | condition='module.matches_regex(r"^api\\b")', 48 | paths=PathsConfig(exclude=["api/", "tests/"]), 49 | ), 50 | ) 51 | 52 | assert expected == result 53 | 54 | 55 | def test_1_allowed_importer_package_name_incl_dot(): 56 | result = dependencies.create_sourcery_custom_rules("app.core", "app.api") 57 | 58 | expected = ( 59 | SourceryCustomRule( 60 | id="dependency-rules-app-core-import", 61 | description="Only `app.api` should import `app.core`", 62 | tags=["architecture", "dependencies"], 63 | pattern="import ..., ${module}, ...", 64 | condition='module.matches_regex(r"^app\.core\\b")', 65 | paths=PathsConfig(exclude=["app/core/", "tests/", "app/api/"]), 66 | ), 67 | SourceryCustomRule( 68 | id="dependency-rules-app-core-from", 69 | description="Only `app.api` should import `app.core`", 70 | tags=["architecture", "dependencies"], 71 | pattern="from ${module} import ...", 72 | condition='module.matches_regex(r"^app\.core\\b")', 73 | paths=PathsConfig(exclude=["app/core/", "tests/", "app/api/"]), 74 | ), 75 | ) 76 | 77 | assert expected == result 78 | 79 | 80 | def test_0_allowed_importer_package_name_with_dot(): 81 | result = dependencies.create_sourcery_custom_rules("app.api", "") 82 | 83 | expected = ( 84 | SourceryCustomRule( 85 | id="dependency-rules-app-api-import", 86 | description="Do not import `app.api` in other packages", 87 | tags=["architecture", "dependencies"], 88 | pattern="import ..., ${module}, ...", 89 | condition='module.matches_regex(r"^app\.api\\b")', 90 | paths=PathsConfig(exclude=["app/api/", "tests/"]), 91 | ), 92 | SourceryCustomRule( 93 | id="dependency-rules-app-api-from", 94 | description="Do not import `app.api` in other packages", 95 | tags=["architecture", "dependencies"], 96 | pattern="from ${module} import ...", 97 | condition='module.matches_regex(r"^app\.api\\b")', 98 | paths=PathsConfig(exclude=["app/api/", "tests/"]), 99 | ), 100 | ) 101 | 102 | assert expected == result 103 | 104 | 105 | def test_1_allowed_importer_package_name_incl_dot_and_underscore(): 106 | result = dependencies.create_sourcery_custom_rules("app.view_util", "app.views") 107 | 108 | expected = ( 109 | SourceryCustomRule( 110 | id="dependency-rules-app-view_util-import", 111 | description="Only `app.views` should import `app.view_util`", 112 | tags=["architecture", "dependencies"], 113 | pattern="import ..., ${module}, ...", 114 | condition='module.matches_regex(r"^app\.view_util\\b")', 115 | paths=PathsConfig(exclude=["app/view_util/", "tests/", "app/views/"]), 116 | ), 117 | SourceryCustomRule( 118 | id="dependency-rules-app-view_util-from", 119 | description="Only `app.views` should import `app.view_util`", 120 | tags=["architecture", "dependencies"], 121 | pattern="from ${module} import ...", 122 | condition='module.matches_regex(r"^app\.view_util\\b")', 123 | paths=PathsConfig(exclude=["app/view_util/", "tests/", "app/views/"]), 124 | ), 125 | ) 126 | 127 | assert expected == result 128 | 129 | 130 | def test_2_allowed_importers_package_name_incl_dot(): 131 | result = dependencies.create_sourcery_custom_rules("app.util", "app.core,app.other") 132 | 133 | expected = ( 134 | SourceryCustomRule( 135 | id="dependency-rules-app-util-import", 136 | description="Only `app.core`, `app.other` should import `app.util`", 137 | tags=["architecture", "dependencies"], 138 | pattern="import ..., ${module}, ...", 139 | condition='module.matches_regex(r"^app\.util\\b")', 140 | paths=PathsConfig( 141 | exclude=["app/util/", "tests/", "app/core/", "app/other/"] 142 | ), 143 | ), 144 | SourceryCustomRule( 145 | id="dependency-rules-app-util-from", 146 | description="Only `app.core`, `app.other` should import `app.util`", 147 | tags=["architecture", "dependencies"], 148 | pattern="from ${module} import ...", 149 | condition='module.matches_regex(r"^app\.util\\b")', 150 | paths=PathsConfig( 151 | exclude=["app/util/", "tests/", "app/core/", "app/other/"] 152 | ), 153 | ), 154 | ) 155 | 156 | assert expected == result 157 | -------------------------------------------------------------------------------- /tests/unit/test_expensive_loop.py: -------------------------------------------------------------------------------- 1 | from sourcery_rules_generator import expensive_loop 2 | from sourcery_rules_generator.models import SourceryCustomRule 3 | 4 | 5 | def test_fully_qualified_function_name(): 6 | result = expensive_loop.create_sourcery_custom_rules("custom_lib.api.create_item") 7 | 8 | expected = ( 9 | SourceryCustomRule( 10 | id="no-custom_lib-api-create_item-for", 11 | description="Don't call `custom_lib.api.create_item()` in loops.", 12 | tags=["performance", "no-custom_lib-api-create_item-in-loops"], 13 | pattern=""" 14 | for ... in ... : 15 | ... 16 | custom_lib.api.create_item(...) 17 | ... 18 | """, 19 | ), 20 | SourceryCustomRule( 21 | id="no-custom_lib-api-create_item-while", 22 | description="Don't call `custom_lib.api.create_item()` in loops.", 23 | tags=["performance", "no-custom_lib-api-create_item-in-loops"], 24 | pattern=""" 25 | while ... : 26 | ... 27 | custom_lib.api.create_item(...) 28 | ... 29 | """, 30 | ), 31 | ) 32 | 33 | assert result == expected 34 | -------------------------------------------------------------------------------- /tests/unit/test_voldemort.py: -------------------------------------------------------------------------------- 1 | from sourcery_rules_generator import voldemort 2 | from sourcery_rules_generator.models import SourceryCustomRule 3 | 4 | 5 | def test_do_not_allow_util(): 6 | result = voldemort.create_sourcery_custom_rules("util") 7 | 8 | expected = ( 9 | SourceryCustomRule( 10 | id="no-util-function-name", 11 | description="Don't use the name util", 12 | tags=["naming", "no-util"], 13 | pattern=""" 14 | def ${function_name}(...): 15 | ... 16 | """, 17 | condition='function_name.contains("util")', 18 | ), 19 | SourceryCustomRule( 20 | id="no-util-function-arg", 21 | description="Don't use the name util", 22 | tags=["naming", "no-util"], 23 | pattern=""" 24 | def ...(...,${arg_name}: ${type?} = ${default_value?},...): 25 | ... 26 | """, 27 | condition='arg_name.contains("util")', 28 | ), 29 | SourceryCustomRule( 30 | id="no-util-class-name", 31 | description="Don't use the name util", 32 | tags=["naming", "no-util"], 33 | pattern=""" 34 | class ${class_name}(...): 35 | ... 36 | """, 37 | condition='class_name.contains("util")', 38 | ), 39 | SourceryCustomRule( 40 | id="no-util-property", 41 | description="Don't use the name util", 42 | tags=["naming", "no-util"], 43 | pattern="${var}: ${type}", 44 | condition='var.contains("util")', 45 | ), 46 | SourceryCustomRule( 47 | id="no-util-variable", 48 | description="Don't use the name util", 49 | tags=["naming", "no-util"], 50 | pattern="${var} = ${value}", 51 | condition='var.contains("util")', 52 | ), 53 | ) 54 | 55 | assert result == expected 56 | -------------------------------------------------------------------------------- /voldemort_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/sourcery-rules-generator/7493fba8c6b0fdc7052088a89476f9b2123e7058/voldemort_create.png --------------------------------------------------------------------------------