├── type_stubs ├── databricks_cli │ ├── sdk │ │ ├── __init__.pyi │ │ ├── api_client.pyi │ │ └── service.pyi │ └── __init__.pyi └── sqlparse │ └── __init__.pyi ├── blackbricks ├── __init__.py ├── files.py ├── blackbricks.py ├── databricks_sync.py └── cli.py ├── .flake8 ├── .pre-commit-hooks.yaml ├── .github ├── stale.yml └── workflows │ └── publish.yml ├── tests └── test_check_test_notebook.py ├── LICENSE ├── .pre-commit-config.yaml ├── pyproject.toml ├── test_notebooks └── test.py ├── CHANGELOG.md ├── .gitignore ├── README.md └── poetry.lock /type_stubs/databricks_cli/sdk/__init__.pyi: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blackbricks/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.2.0" 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E203,E301,E302,W503,E704 3 | -------------------------------------------------------------------------------- /type_stubs/databricks_cli/__init__.pyi: -------------------------------------------------------------------------------- 1 | def initialize_cli_for_databricks_notebooks() -> None: ... 2 | -------------------------------------------------------------------------------- /type_stubs/databricks_cli/sdk/api_client.pyi: -------------------------------------------------------------------------------- 1 | class ApiClient: 2 | def __init__(self, *, host: str, token: str) -> None: ... 3 | -------------------------------------------------------------------------------- /type_stubs/sqlparse/__init__.pyi: -------------------------------------------------------------------------------- 1 | from typing import Literal, Union 2 | 3 | def format( 4 | sql: str, *, reindent: bool, keyword_case: Union[Literal["upper"], Literal["lower"]] 5 | ) -> str: ... 6 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: blackbricks 2 | name: blackbricks 3 | description: "Black for Databricks notebooks" 4 | entry: blackbricks 5 | language: python 6 | language_version: python3 7 | require_serial: true 8 | types: [python] 9 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 60 2 | daysUntilClose: 7 3 | exemptLabels: 4 | - pinned 5 | - security 6 | staleLabel: wontfix 7 | markComment: > 8 | This issue has been automatically marked as stale because it has not had 9 | recent activity. It will be closed if no further activity occurs. Thank you 10 | for your contributions. 11 | closeComment: false 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish version to PyPI 2 | on: 3 | push: 4 | tags: 5 | - "*.*.*" 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Build and publish to pypi 12 | uses: JRubics/poetry-publish@v1.13 13 | with: 14 | pypi_token: ${{ secrets.PYPI_PASSWORD }} 15 | -------------------------------------------------------------------------------- /tests/test_check_test_notebook.py: -------------------------------------------------------------------------------- 1 | from contextlib import redirect_stdout 2 | from io import StringIO 3 | from pathlib import Path 4 | 5 | from blackbricks.blackbricks import FormatConfig 6 | from blackbricks.cli import process_files 7 | from blackbricks.files import LocalFile 8 | 9 | 10 | def test_check_test_notebook(): 11 | 12 | f = StringIO() 13 | with redirect_stdout(f): 14 | 15 | process_files( 16 | [LocalFile(Path(__file__).parent.parent / "test_notebooks" / "test.py")], 17 | format_config=FormatConfig(), 18 | check=True, 19 | ) 20 | assert f.getvalue() == "All done!\n1 files would be left unchanged\n" 21 | -------------------------------------------------------------------------------- /type_stubs/databricks_cli/sdk/service.pyi: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List, Literal, TypedDict 3 | 4 | from _typeshed import Incomplete 5 | 6 | from .api_client import ApiClient 7 | 8 | class ObjectType(Enum): 9 | directory = "DIRECTORY" 10 | notebook = "NOTEBOOK" 11 | library = "LIBRARY" 12 | repo = "REPO" 13 | 14 | class ListEntry(TypedDict): 15 | path: str 16 | object_type: ObjectType 17 | 18 | class ListReponse(TypedDict): 19 | objects: List[ListEntry] 20 | 21 | class ExportResponse(TypedDict): 22 | content: str 23 | 24 | class WorkspaceService: 25 | client: Incomplete 26 | def __init__(self, client: ApiClient) -> None: ... 27 | def list(self, path: str) -> ListReponse: ... 28 | def import_workspace( 29 | self, 30 | path: str, 31 | *, 32 | format: Literal["SOURCE"], 33 | language: Literal["PYTHON"], 34 | content: str, 35 | overwrite: bool 36 | ) -> None: ... 37 | def export_workspace( 38 | self, path: str, *, format: Literal["SOURCE"] 39 | ) -> ExportResponse: ... 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Inspera AS, by Bendik Samseth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: blackbricks 5 | name: blackbricks 6 | language: system 7 | entry: poetry run blackbricks 8 | require_serial: true 9 | types: [python] 10 | files: ^test_notebooks/ 11 | 12 | - id: black 13 | name: black 14 | language: system 15 | entry: poetry run black 16 | types: [python] 17 | exclude: ^test_notebooks/ 18 | 19 | - id: isort 20 | name: isort 21 | language: system 22 | entry: poetry run isort 23 | types: [python] 24 | exclude: ^test_notebooks/ 25 | 26 | - id: flake8 27 | name: flake8 28 | language: system 29 | entry: poetry run flake8 30 | types: [python] 31 | exclude: ^test_notebooks/ 32 | 33 | - id: mypy 34 | name: mypy 35 | language: system 36 | entry: poetry run mypy 37 | types: [python] 38 | exclude: ^test_notebooks/ 39 | 40 | - id: pytest 41 | name: pytest 42 | language: system 43 | entry: poetry run pytest 44 | types: [python] 45 | pass_filenames: false 46 | 47 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "blackbricks" 3 | version = "2.2.0" 4 | description = "Black for Databricks notebooks" 5 | authors = ["Bendik Samseth "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/inspera/blackbricks" 9 | keywords = [ 10 | "automation", 11 | "formatter", 12 | "black", 13 | "sql", 14 | "yapf", 15 | "autopep8", 16 | "pyfmt", 17 | "gofmt", 18 | "rustfmt", 19 | ] 20 | classifiers = [ 21 | "Development Status :: 4 - Beta", 22 | "Environment :: Console", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3 :: Only", 31 | "Topic :: Software Development :: Quality Assurance", 32 | ] 33 | 34 | [tool.poetry.scripts] 35 | blackbricks = "blackbricks.cli:app" 36 | 37 | [tool.poetry.dependencies] 38 | python = "^3.8" 39 | black = "^24.4.2" 40 | sqlparse = "^0.4.2" 41 | databricks-cli = "^0.17.3" 42 | typer = { extras = ["all"], version = ">=0.6.1,<0.8" } 43 | 44 | [tool.poetry.dev-dependencies] 45 | pytest = "^7.1.3" 46 | isort = "^5.10.1" 47 | flake8 = "^3.9.2" 48 | commitizen = "^2.32.2" 49 | mypy = "^0.971" 50 | 51 | 52 | [tool.commitizen] 53 | name = "cz_conventional_commits" 54 | version = "2.2.0" 55 | tag_format = "$version" 56 | version_files = ["blackbricks/__init__.py:version", "pyproject.toml:version"] 57 | 58 | [tool.isort] 59 | profile = "black" 60 | 61 | [tool.mypy] 62 | strict = true 63 | mypy_path = "type_stubs" 64 | 65 | 66 | [build-system] 67 | requires = ["poetry-core>=1.0.0"] 68 | build-backend = "poetry.core.masonry.api" 69 | -------------------------------------------------------------------------------- /test_notebooks/test.py: -------------------------------------------------------------------------------- 1 | # Databricks notebook source 2 | from pyspark.sql import SQLContext 3 | 4 | sqlContext = SQLContext(spark) 5 | 6 | # COMMAND ---------- 7 | 8 | # DBTITLE 1,Title for a python cell 9 | dw_management = ( 10 | spark.table("this.that") 11 | .select("some_id") 12 | .join(spark.table("other.that"), "other_id") 13 | ) 14 | 15 | 16 | def test_func(input_param): 17 | """ 18 | :param input_param: input 19 | """ 20 | return None 21 | 22 | # COMMAND ---------- 23 | 24 | # MAGIC %md 25 | # MAGIC # Markdown heading! 26 | # MAGIC - And a list element 27 | 28 | # COMMAND ---------- 29 | 30 | # MAGIC %sh 31 | # MAGIC ls -l 32 | 33 | # COMMAND ---------- 34 | 35 | # MAGIC %sql 36 | # MAGIC ALTER TABLE this.that 37 | # MAGIC SET TBLPROPERTIES (delta.autoOptimize = TRUE); 38 | # MAGIC 39 | # MAGIC 40 | # MAGIC ALTER TABLE other.that 41 | # MAGIC SET TBLPROPERTIES (delta.autoOptimize = TRUE); 42 | 43 | # COMMAND ---------- 44 | 45 | # DBTITLE 1,Title for an SQL cell 46 | # MAGIC %sql 47 | # MAGIC SELECT country, 48 | # MAGIC product, 49 | # MAGIC SUM(profit) 50 | # MAGIC FROM sales 51 | # MAGIC LEFT JOIN x ON x.id=sales.k 52 | # MAGIC GROUP BY country, 53 | # MAGIC product 54 | # MAGIC HAVING f > 7 55 | # MAGIC AND fk=9 56 | # MAGIC LIMIT 5; 57 | 58 | # COMMAND ---------- 59 | 60 | # MAGIC %sql -- nofmt 61 | # MAGIC CREATE OR REPLACE VIEW abc.test AS 62 | # MAGIC (SELECT foo.bar, 63 | # MAGIC foo.fizz, -- a comment 64 | # MAGIC foo.fizzbar, 65 | # MAGIC foo.bazz 66 | # MAGIC FROM cba.tset foo); 67 | # MAGIC 68 | # MAGIC 69 | # MAGIC CREATE OR REPLACE VIEW asd.dsa AS 70 | # MAGIC (SELECT bar.foo, 71 | # MAGIC COLLECT_SET(bar.fizz)[0], 72 | # MAGIC FIRST(bar.baz) 73 | # MAGIC FROM dsa.asd bar 74 | # MAGIC GROUP BY bar.fizzbuzz); 75 | 76 | # COMMAND ---------- 77 | 78 | # MAGIC %sql 79 | # MAGIC SELECT id 80 | # MAGIC FROM test_table 81 | # MAGIC LIMIT 1 82 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.0 (2024-08-02) 2 | 3 | ### Feat 4 | 5 | - upgrade to black's 2024 release 6 | 7 | ## 2.1.3 (2023-07-24) 8 | 9 | ### Fix 10 | 11 | - **deps**: bump pygments from 2.14.0 to 2.15 (#45) 12 | 13 | ## 2.1.2 (2023-05-23) 14 | 15 | ### Fix 16 | 17 | - **deps**: bump requests from 2.28.2 to 2.31.0 (#41) 18 | 19 | ## 2.1.1 (2023-04-26) 20 | 21 | ### Fix 22 | 23 | - **deps**: bump sqlparse from 0.4.3 to 0.4.4 (#40) 24 | 25 | ## 2.1.0 (2023-03-26) 26 | 27 | ### Feat 28 | 29 | - upgrade to black's 2023 release 30 | 31 | ## 2.0.0 (2023-02-10) 32 | 33 | ### BREAKING CHANGE 34 | 35 | - add newline at the end of formatted notebooks (#36) 36 | 37 | ## 1.0.5 (2023-02-07) 38 | 39 | ### Fix 40 | 41 | - **deps**: bump oauthlib from 3.2.1 to 3.2.2 (#35) 42 | 43 | ## 1.0.4 (2023-01-31) 44 | 45 | ### Fix 46 | 47 | - avoid failing on quoted cell dividers 48 | 49 | ## 1.0.3 (2023-01-31) 50 | 51 | Resolve buf where blackbricks removed sql code on the same line as the `%sql` magic. See #32. 52 | 53 | ## 1.0.2 (2023-01-10) 54 | 55 | Bumping dependencies to resolve dependabot alert. See #30. 56 | 57 | ## 1.0.1 (2022-10-28) 58 | 59 | Non-functional patch with minor documentation updates. 60 | 61 | ## 1.0.0 (2022-09-12) 62 | 63 | ### Fix 64 | 65 | - respect `insecure` option in .databrickscfg (#28) 66 | - restore Python 3.8 compatibility 67 | - handle trailing whitespace the same as Databricks 68 | - add strict mypy typing to project 69 | - ignore non-text files 70 | 71 | ### Feat 72 | 73 | - recursively discover remote notebooks 74 | - remove option for two-space indentation 75 | 76 | ### BREAKING CHANGE 77 | 78 | Removal of two-space indentation as an option and the change to forcing the PEP-8 compliant four-space indentation. 79 | 80 | See [#25](https://github.com/inspera/blackbricks/pull/25) for background. 81 | 82 | ## 0.7.0 (2022-09-03) 83 | 84 | ### Feat 85 | 86 | - deprecate two-space indentation 87 | 88 | ## 0.6.7 (2021-08-04) 89 | 90 | ### Fix 91 | 92 | - gracefully skip emtpy files 93 | 94 | ## 0.6.6 (2021-05-27) 95 | 96 | ### Fix 97 | 98 | - handle cell titles in magic cells 99 | 100 | ## 0.6.5 (2021-05-27) 101 | 102 | ### Fix 103 | 104 | - update docstring handling with upstream changes in black 105 | 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | .idea 137 | .python-version 138 | -------------------------------------------------------------------------------- /blackbricks/files.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | from typing import List 4 | 5 | import typer 6 | 7 | from .databricks_sync import DatabricksAPI, ObjectType 8 | 9 | 10 | @dataclass 11 | class File: 12 | path: str 13 | 14 | @property 15 | def content(self) -> str: 16 | raise NotImplementedError( 17 | "Do not use this class directly, use one of its subclasses." 18 | ) 19 | 20 | @content.setter 21 | def content(self, _: str, /) -> None: 22 | raise NotImplementedError( 23 | "Do not use this class directly, use one of its subclasses." 24 | ) 25 | 26 | 27 | class LocalFile(File): 28 | @property 29 | def content(self) -> str: 30 | with open(self.path) as f: 31 | return f.read() 32 | 33 | @content.setter 34 | def content(self, new_content: str, /) -> None: 35 | with open(self.path, "w") as f: 36 | f.write(new_content) 37 | 38 | 39 | @dataclass 40 | class RemoteNotebook(File): 41 | api_client: DatabricksAPI 42 | 43 | @property 44 | def content(self) -> str: 45 | """ 46 | Get the content of the notebook as a string. 47 | 48 | Note: Databricks will not include a trailing newlines from a notebook. 49 | That is, even if you checkout a notebook from a repo where there is a trailing 50 | newline, Databricks won't "show" an empty line in the UI. And similarly, this 51 | API doesn't include it. This contrasts with the prefered style of terminating 52 | the files with a newline, as enforced by black and blackbricks. To avoid 53 | perpetually reporting diffs due to newlines, we add a trailing newline to the 54 | content, _assuming_ that the 55 | we assume it to be present and add it back if it is missing. Failing to do this 56 | will cause blackbricks to perpetually report that it wants to add a trailing 57 | newline to remote notebooks. 58 | """ 59 | return self.api_client.read_notebook(self.path) + "\n" 60 | 61 | @content.setter 62 | def content(self, new_content: str, /) -> None: 63 | """ 64 | Set the content of the notebook to the given string. 65 | 66 | Note: We do _not_ need to do the inverse handling of trailing newlines as in 67 | the getter. The new content here is presumably the output of blackbricks, and 68 | we should let Databricks import that from the same source as it would find by 69 | checking out a repo notebook that has been formatted with blackbricks. 70 | """ 71 | self.api_client.write_notebook(self.path, new_content) 72 | 73 | 74 | def resolve_filepaths(paths: List[str]) -> List[str]: 75 | """Resolve the paths given into valid file names 76 | 77 | Directories are recursively added, similarly to how black operates. 78 | 79 | :param paths: List of paths to files or directories. 80 | :return: Absolute paths to all files given, including all files in any 81 | directories given, including subdirectories. 82 | """ 83 | file_paths = [] 84 | while paths: 85 | path = os.path.abspath(paths.pop()) 86 | 87 | if not os.path.exists(path): 88 | typer.echo( 89 | typer.style("Error:", fg=typer.colors.RED) 90 | + " No such file or directory: " 91 | + typer.style(path, fg=typer.colors.CYAN) 92 | ) 93 | raise typer.Exit(1) 94 | 95 | if os.path.isdir(path): 96 | # Recursively add all the files/dirs in path to the paths to be consumed. 97 | paths.extend([os.path.join(path, f) for f in os.listdir(path)]) 98 | 99 | else: 100 | file_paths.append(path) 101 | 102 | return file_paths 103 | 104 | 105 | def resolve_databricks_paths( 106 | paths: List[str], *, api_client: DatabricksAPI 107 | ) -> List[str]: 108 | """Resolve the remote paths given into valid file names 109 | 110 | Directories are recursively added, similarly to how black operates. 111 | 112 | :param paths: List of paths to remote files or directories. 113 | :api_client: Databricks API client to use. 114 | :return: Absolute paths to all files given, including all files in any 115 | directories given, including subdirectories. 116 | """ 117 | paths = [api_client._resolve_path(path) for path in paths] 118 | file_paths = [] 119 | while paths: 120 | path = paths.pop() 121 | response = api_client.list_workspace(path) 122 | for file_obj in response: 123 | if (obj_type := file_obj["object_type"]) == ObjectType.notebook.value: 124 | file_paths.append(file_obj["path"]) 125 | elif obj_type in (ObjectType.directory.value, ObjectType.repo.value): 126 | paths.append(file_obj["path"]) 127 | return file_paths 128 | -------------------------------------------------------------------------------- /blackbricks/blackbricks.py: -------------------------------------------------------------------------------- 1 | import re 2 | from dataclasses import dataclass 3 | from typing import Literal, Union 4 | 5 | import black 6 | import sqlparse 7 | 8 | COMMAND = "# COMMAND ----------" 9 | HEADER = "# Databricks notebook source" 10 | NOFMT = "-- nofmt" 11 | CELL_TITLE = "# DBTITLE 1," 12 | 13 | 14 | @dataclass(frozen=True) 15 | class FormatConfig: 16 | "Data-only class to hold format configuration options and their defaults." 17 | line_length: int = black.const.DEFAULT_LINE_LENGTH 18 | sql_upper: bool = True 19 | 20 | 21 | def format_str(content: str, config: FormatConfig = FormatConfig()) -> str: 22 | """ 23 | Format the content of a notebook according to the format config provided. 24 | 25 | This assumes that `content` is the _full_ content of a notebook file, and that 26 | the notebook is a Python notebook. 27 | 28 | :param content: A string holding the entire content of a notebook. 29 | :param config: An object holding the desired formatting options. 30 | :return: The content of the file, formatted according to the configuration. 31 | """ 32 | content = content.replace(HEADER, "", 1) 33 | cells = re.split(f"^{COMMAND}.*$", content, flags=re.MULTILINE) 34 | 35 | output_cells = [] 36 | for cell in cells: 37 | cell = cell.strip() 38 | 39 | if "# MAGIC %sql" in cell: 40 | output_cells.append( 41 | _format_sql_cell( 42 | cell, sql_keyword_case="upper" if config.sql_upper else "lower" 43 | ) 44 | ) 45 | elif "# MAGIC" in cell: 46 | output_cells.append(cell) # Generic magic cell - output as-is. 47 | else: 48 | output_cells.append( 49 | black.format_str( 50 | cell, mode=black.FileMode(line_length=config.line_length) 51 | ) 52 | ) 53 | 54 | output = ( 55 | f"{HEADER}\n" 56 | + f"\n\n{COMMAND}\n\n".join( 57 | "".join(line.rstrip() + "\n" for line in cell.splitlines()).rstrip() 58 | for cell in output_cells 59 | ).rstrip() 60 | ) 61 | 62 | # Databricks adds a space after '# MAGIC', regardless of wheter this constitutes 63 | # trailing whitespace. To avoid perpetual differences, blackbricks also adds this 64 | # extra whitespace. This is only added if the line does not contain anything else. 65 | # In all other cases, blackbricks will remove trailing whitespace. 66 | output_ws_normalized = "" 67 | for line in output.splitlines(): 68 | line = line.rstrip() 69 | 70 | if line == "# MAGIC": 71 | line += " " 72 | 73 | output_ws_normalized += line + "\n" 74 | 75 | return output_ws_normalized.strip() + "\n" 76 | 77 | 78 | def _format_sql_cell( 79 | cell: str, sql_keyword_case: Union[Literal["upper"], Literal["lower"]] = "upper" 80 | ) -> str: 81 | """ 82 | Format a MAGIC %sql cell. 83 | 84 | :param cell: The content of an SQL cell. 85 | :param sql_keyword_case: One of ["upper", "lower"], setting the case for SQL keywords. 86 | :return: The cell with formatting applied. 87 | """ 88 | 89 | # Cells can have a title. This should just be kept at the start of the cell. 90 | if CELL_TITLE in cell.lstrip().splitlines()[0]: 91 | title_line = cell.lstrip().splitlines()[0] + "\n" 92 | cell = cell[len(title_line) :] 93 | else: 94 | title_line = "" 95 | 96 | # Formatting can be disabled on a cell-by-cell basis by adding `-- nofmt` to the first line. 97 | if NOFMT in cell.strip().splitlines()[0]: 98 | return title_line + cell 99 | 100 | sql_lines = [] 101 | for line in (line.strip() for line in cell.strip().splitlines()): 102 | if line == "# MAGIC %sql": 103 | continue 104 | elif line.startswith("# MAGIC %sql"): 105 | line = line.replace(" %sql", "", 1) 106 | sql = line.split()[2:] # Remove "# MAGIC". 107 | sql_lines.append(" ".join(sql).strip()) 108 | 109 | return ( 110 | title_line 111 | + "# MAGIC %sql\n" 112 | + "\n".join( 113 | f"# MAGIC {sql}" 114 | for sql in sqlparse.format( 115 | "\n".join(sql_lines).strip(), 116 | reindent=True, 117 | keyword_case=sql_keyword_case, 118 | ).splitlines() 119 | ) 120 | ) 121 | 122 | 123 | def unified_diff(a: str, b: str, a_name: str, b_name: str) -> str: 124 | """ 125 | Return a unified diff string between strings `a` and `b`. 126 | 127 | :param a: The first string (e.g. before). 128 | :param b: The second string (e.g. after). 129 | :param a_name: The "filename" to display for the `a` string. 130 | :param b_name: The "filename" to display for the `b` string. 131 | :return: A `git diff` like diff of the two strings. 132 | """ 133 | import difflib 134 | 135 | a_lines = [line + "\n" for line in a.split("\n")] 136 | b_lines = [line + "\n" for line in b.split("\n")] 137 | return "".join( 138 | difflib.unified_diff(a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5) 139 | ) 140 | -------------------------------------------------------------------------------- /blackbricks/databricks_sync.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | from configparser import ConfigParser 4 | from enum import Enum 5 | from typing import List, Optional, TypedDict, cast 6 | 7 | import typer 8 | from databricks_cli.sdk.api_client import ApiClient 9 | from databricks_cli.sdk.service import WorkspaceService 10 | 11 | 12 | class ObjectType(str, Enum): 13 | directory = "DIRECTORY" 14 | notebook = "NOTEBOOK" 15 | library = "LIBRARY" 16 | repo = "REPO" 17 | 18 | 19 | class ListEntry(TypedDict): 20 | path: str 21 | object_type: ObjectType 22 | 23 | 24 | class DatabricksAPI: 25 | def __init__( 26 | self, 27 | databricks_host: str, 28 | databricks_token: str, 29 | username: Optional[str] = None, 30 | verify_ssl: bool = True, 31 | ) -> None: 32 | self.client = WorkspaceService( 33 | ApiClient(host=databricks_host, token=databricks_token, verify=verify_ssl) 34 | ) 35 | self.username = username 36 | 37 | def _resolve_path(self, path: str) -> str: 38 | """ 39 | Resolve path to notebook. 40 | 41 | If `path` is not absolute and a username is in use, interpret the path as 42 | relative to the user direcotry. 43 | 44 | Example: 45 | 46 | tmp/notebook.py -> /Users/{username}/tmp/notebook.py 47 | /tmp/notebook.py -> /tmp/notebook.py 48 | """ 49 | if not path.startswith("/") and self.username is not None: 50 | path = f"/Users/{self.username}/{path}" 51 | return path 52 | 53 | def read_notebook(self, path: str) -> str: 54 | path = self._resolve_path(path) 55 | response = self.client.export_workspace(path, format="SOURCE") 56 | return base64.decodebytes(response["content"].encode()).decode() 57 | 58 | def write_notebook(self, path: str, content: str) -> None: 59 | path = self._resolve_path(path) 60 | self.client.import_workspace( 61 | path, 62 | format="SOURCE", 63 | language="PYTHON", 64 | content=base64.b64encode(content.encode()).decode(), 65 | overwrite=True, 66 | ) 67 | 68 | def list_workspace(self, path: str) -> List[ListEntry]: 69 | return cast(List[ListEntry], self.client.list(path)["objects"]) 70 | 71 | 72 | def get_api_client(profile_name: str) -> DatabricksAPI: 73 | config = ConfigParser() 74 | config_path = os.path.expanduser("~/.databrickscfg") 75 | try: 76 | config.read(config_path) 77 | except FileNotFoundError: 78 | typer.echo( 79 | typer.style("Error:", fg=typer.colors.RED, bold=True) 80 | + typer.style( 81 | " Your ~/.databrickscfg file is missing.", 82 | bold=True, 83 | ), 84 | ) 85 | typer.echo( 86 | "If you haven't already, first install the databricks cli,\n" 87 | "then run `databricks configure` and check your ~/.databrickscfg file." 88 | ) 89 | raise typer.Abort() 90 | except Exception as e: 91 | typer.echo( 92 | typer.style("Error:", fg=typer.colors.RED, bold=True) 93 | + typer.style( 94 | " An error occurred while reading your ~/.databrickscfg file.", 95 | bold=True, 96 | ) 97 | ) 98 | raise e 99 | 100 | if profile_name != "DEFAULT" and not config.has_section(profile_name): 101 | typer.echo( 102 | typer.style("Error:", fg=typer.colors.RED, bold=True) 103 | + typer.style( 104 | f" Could not find {profile_name} in ~/.databrickscfg", bold=True 105 | ), 106 | ) 107 | typer.echo( 108 | "Run `databricks configure` and/or check your ~/.databrickscfg file." 109 | ) 110 | raise typer.Abort() 111 | 112 | host = config.get(profile_name, "host") # Will throw if missing, always needed. 113 | username = config.get(profile_name, "username", fallback=None) 114 | token = config.get(profile_name, "token", fallback=None) 115 | password = config.get(profile_name, "password", fallback=None) 116 | insecure = config.get(profile_name, "insecure", fallback=None) 117 | credentials = token if token is not None else password 118 | 119 | # Handle no username: 120 | if username is None: 121 | typer.echo( 122 | typer.style("Warning:", fg=typer.colors.YELLOW, bold=True) 123 | + typer.style(" No username in ~/.databrickscfg.", bold=True) 124 | ) 125 | typer.echo( 126 | "Please add `username = ...` to your profile (and/or the [DEFAULT] profile)" 127 | ", or be sure to always write full paths to notebooks." 128 | ) 129 | 130 | # Handle no token and no password: 131 | if credentials is None: 132 | typer.echo( 133 | typer.style("Error:", fg=typer.colors.RED, bold=True) 134 | + typer.style( 135 | f" Found neither token nor password in ~/.databrickscfg for profile {profile_name}", 136 | bold=True, 137 | ), 138 | ) 139 | raise typer.Abort() 140 | 141 | # Handle password without a username: 142 | if token is None and password is not None and username is None: 143 | typer.echo( 144 | typer.style("Error:", fg=typer.colors.RED, bold=True) 145 | + typer.style( 146 | f" Profile {profile_name} is missing a username in ~/.databrickscfg", 147 | bold=True, 148 | ), 149 | ) 150 | raise typer.Abort() 151 | 152 | # Handle both token and password (one might be from DEFAULT): 153 | if token is not None and password is not None: 154 | if password != config.defaults().get("password"): 155 | credentials = password 156 | if token != config.defaults().get("token"): 157 | credentials = token 158 | 159 | return DatabricksAPI( 160 | databricks_host=host, 161 | databricks_token=credentials, 162 | username=username, 163 | verify_ssl=insecure is None, 164 | ) 165 | -------------------------------------------------------------------------------- /blackbricks/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import textwrap 3 | import warnings 4 | from typing import List, NoReturn, Optional, Sequence 5 | 6 | import black 7 | import typer 8 | 9 | from . import __version__ 10 | from .blackbricks import HEADER, FormatConfig, format_str, unified_diff 11 | from .databricks_sync import get_api_client 12 | from .files import ( 13 | File, 14 | LocalFile, 15 | RemoteNotebook, 16 | resolve_databricks_paths, 17 | resolve_filepaths, 18 | ) 19 | 20 | app = typer.Typer(add_completion=False) 21 | 22 | 23 | def process_files( 24 | files: Sequence[File], 25 | format_config: FormatConfig = FormatConfig(), 26 | diff: bool = False, 27 | check: bool = False, 28 | ) -> int: 29 | no_change = True 30 | n_changed_files = 0 31 | n_notebooks = 0 32 | 33 | for file_ in files: 34 | try: 35 | content = file_.content 36 | except UnicodeDecodeError: 37 | # File is not a text file. Probably a binary file. Skip. 38 | continue 39 | 40 | if not content.lstrip() or HEADER not in content.lstrip().splitlines()[0]: 41 | # Not a Databricks notebook - skip 42 | continue 43 | 44 | n_notebooks += 1 45 | output = format_str(content, config=format_config) 46 | 47 | no_change &= output == content 48 | n_changed_files += output != content 49 | 50 | if diff: 51 | diff_output = unified_diff( 52 | content, 53 | output, 54 | f"{os.path.basename(file_.path)} (before)", 55 | f"{os.path.basename(file_.path)} (after)", 56 | ) 57 | if diff_output.strip(): 58 | typer.echo(diff_output) 59 | elif not check: 60 | file_.content = output 61 | 62 | if output != content: 63 | typer.secho(f"reformatted {file_.path}", bold=True) 64 | elif check and output != content: 65 | typer.secho(f"would reformat {file_.path}", bold=True) 66 | 67 | unchanged_number = typer.style( 68 | str(n_notebooks - n_changed_files), fg=typer.colors.GREEN 69 | ) 70 | changed_number = typer.style(str(n_changed_files), fg=typer.colors.MAGENTA) 71 | unchanged_echo = ( 72 | f"{unchanged_number} files {'would be ' if check or diff else ''}left unchanged" 73 | ) 74 | changed_echo = typer.style( 75 | f"{changed_number} files {'would be ' if check or diff else ''}reformatted", 76 | bold=True, 77 | ) 78 | 79 | typer.secho("All done!", bold=True) 80 | typer.echo( 81 | ", ".join( 82 | filter( 83 | lambda s: bool(s), 84 | [ 85 | changed_echo if n_changed_files else "", 86 | unchanged_echo if n_notebooks - n_changed_files > 0 else "", 87 | ], 88 | ) 89 | ) 90 | ) 91 | return n_changed_files 92 | 93 | 94 | def mutually_exclusive(names: List[str], values: List[bool]) -> None: 95 | if sum(values) > 1: 96 | names_formatted = ", ".join( 97 | typer.style(name, fg=typer.colors.CYAN) for name in names 98 | ) 99 | typer.echo( 100 | f"{typer.style('Error:', fg=typer.colors.RED)} " 101 | + f"Only one of {names_formatted} may be use at the same time." 102 | ) 103 | raise typer.Exit(1) 104 | 105 | 106 | def version_callback(version_requested: bool) -> None: 107 | "Display versioin information and exit" 108 | if version_requested: 109 | version = typer.style(__version__, fg=typer.colors.GREEN) 110 | typer.echo(f"blackbricks, version {version}") 111 | raise typer.Exit() 112 | 113 | 114 | @app.command() 115 | def main( 116 | filenames: List[str] = typer.Argument( 117 | None, help="Path to the notebook(s) to format." 118 | ), 119 | remote_filenames: bool = typer.Option( 120 | False, 121 | "--remote", 122 | "-r", 123 | help="If this option is used, all filenames are treated as paths to " 124 | "notebooks on your Databricks host (i.e. not local files).", 125 | ), 126 | databricks_profile: str = typer.Option( 127 | "DEFAULT", 128 | "--profile", 129 | "-p", 130 | metavar="NAME", 131 | help="If using --remote, which Databricks profile to use.", 132 | ), 133 | line_length: int = typer.Option( 134 | black.const.DEFAULT_LINE_LENGTH, help="How many characters per line to allow." 135 | ), 136 | sql_upper: bool = typer.Option( 137 | True, help="SQL keywords should be UPPERCASE or lowercase." 138 | ), 139 | check: bool = typer.Option( 140 | False, 141 | "--check", 142 | help="Don't write the files back, just return the status. " 143 | "Return code 0 means nothing would change.", 144 | show_default=False, 145 | ), 146 | diff: bool = typer.Option( 147 | False, 148 | "--diff", 149 | help="Don't write the files back, just output a diff for each file on stdout.", 150 | show_default=False, 151 | ), 152 | version: bool = typer.Option( 153 | None, 154 | "--version", 155 | is_eager=True, 156 | callback=version_callback, 157 | help="Display version information and exit.", 158 | ), 159 | ) -> NoReturn: 160 | """ 161 | Formatting tool for Databricks python notebooks. 162 | 163 | Python cells are formatted using `black`, and SQL cells are formatted by `sqlparse`. 164 | 165 | Local files (without the `--remote` option): 166 | 167 | - Only files that look like Databricks (Python) notebooks will be processed. That is, they must start with the header `# Databricks notebook source` 168 | 169 | - If you specify a directory as one of the file names, all files in that directory will be added, including any subdirectory. 170 | 171 | Remote files (with the `--remote` option): 172 | 173 | - Make sure you have installed the Databricks CLI (``pip install databricks_cli``) 174 | 175 | - Make sure you have configured at least one profile (`databricks configure`). Check the file `~/.databrickscfg` if you are not sure. 176 | 177 | - File paths should start with `/`. Otherwise they are interpreted as relative to `/Users/username`, where `username` is the username specified in the Databricks profile used. 178 | """ 179 | assert not version, "If version is set, we don't get here." 180 | 181 | mutually_exclusive(["--check", "--diff"], [check, diff]) 182 | 183 | if not filenames: 184 | typer.secho("No Path provided. Nothing to do.", bold=True) 185 | raise typer.Exit() 186 | 187 | files: List[File] 188 | if remote_filenames: 189 | api_client = get_api_client(databricks_profile) 190 | files = [ 191 | RemoteNotebook(fname, api_client) 192 | for fname in resolve_databricks_paths(filenames, api_client=api_client) 193 | ] 194 | else: 195 | files = [LocalFile(fname) for fname in resolve_filepaths(filenames)] 196 | 197 | n_changed_files = process_files( 198 | files, 199 | format_config=FormatConfig(line_length=line_length, sql_upper=sql_upper), 200 | diff=diff, 201 | check=check, 202 | ) 203 | raise typer.Exit(n_changed_files if check else 0) 204 | 205 | 206 | if __name__ == "__main__": 207 | app() 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://img.shields.io/pypi/v/blackbricks.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/blackbricks/) 2 | [![Downloads](https://pepy.tech/badge/blackbricks)](https://pepy.tech/project/blackbricks) 3 | [![Downloads per month](https://pepy.tech/badge/blackbricks/month)](https://pepy.tech/project/blackbricks/month) 4 | [![License](https://img.shields.io/pypi/l/blackbricks)](LICENSE) 5 | [![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | 7 | # Blackbricks 8 | 9 | A formatting tool for your Databricks notebooks. 10 | 11 | - Python cells are formatted with [black](https://github.com/psf/black) 12 | - SQL cells are formatted with [sqlparse](https://github.com/andialbrecht/sqlparse) 13 | 14 | ## Table of Contents 15 | 16 | * [Installation](#installation) 17 | * [Usage](#usage) 18 | * [Version control integration](#version-control-integration) 19 | * [Contributing](#contributing) 20 | * [FAQ](#faq) 21 | * [Breaking changes](#breaking-changes) 22 | 23 | ## Installation 24 | 25 | While you can use `pip` directly, you should prefer using [pipx](https://pypa.github.io/pipx/). 26 | 27 | ```bash 28 | $ pipx install blackbricks 29 | ``` 30 | 31 | You probably also want to have installed the `databricks-cli`, in order to use `blackbricks` directly on your notebooks. 32 | 33 | ``` bash 34 | $ pipx install databricks-cli 35 | $ databricks configure # Required in order to use `blackbricks` on remote notebooks. 36 | ``` 37 | 38 | ## Usage 39 | You can use `blackbricks` on Python notebook files stored locally, or directly on the notebooks stored in Databricks. 40 | 41 | For the most part, `blackbricks` operates very similarly to `black`. 42 | 43 | ``` bash 44 | $ blackbricks notebook1.py notebook2.py # Formats both notebooks. 45 | $ blackbricks notebook_directory/ # Formats every notebook under the directory (recursively). 46 | ``` 47 | An important difference is that `blackbricks` will ignore any file that does not contain the `# Databricks notebook 48 | source` header on the first line. Databricks adds this line to all Python notebooks. This means you can happily run 49 | `blackbricks` on a directory with both notebooks and regular Python files, and `blackbricks` won't touch the latter. 50 | 51 | If you specify the `-r` or `--remote` flag, `blackbricks` will work directly on your notebooks stored in Databricks. 52 | 53 | ``` bash 54 | $ blackbricks --remote /Users/username/notebook.py 55 | $ blackbricks --remote /Repos/username/repo-name/notebook.py 56 | ``` 57 | 58 | ### Full usage 59 | 60 | ```text 61 | $ poetry run blackbricks --help 62 | 63 | Usage: blackbricks [OPTIONS] [FILENAMES]... 64 | 65 | Formatting tool for Databricks python notebooks. 66 | Python cells are formatted using `black`, and SQL cells are formatted by `sqlparse`. 67 | Local files (without the `--remote` option): 68 | - Only files that look like Databricks (Python) notebooks will be processed. That is, 69 | they must start with the header `# Databricks notebook source` 70 | - If you specify a directory as one of the file names, all files in that directory will 71 | be added, including any subdirectory. 72 | Remote files (with the `--remote` option): 73 | - Make sure you have installed the Databricks CLI (``pip install databricks_cli``) 74 | - Make sure you have configured at least one profile (`databricks configure`). Check the 75 | file `~/.databrickscfg` if you are not sure. 76 | - File paths should start with `/`. Otherwise they are interpreted as relative to 77 | `/Users/username`, where `username` is the username specified in the Databricks profile 78 | used. 79 | 80 | ╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮ 81 | │ filenames [FILENAMES]... Path to the notebook(s) to format. [default: None] │ 82 | ╰────────────────────────────────────────────────────────────────────────────────────────╯ 83 | ╭─ Options ──────────────────────────────────────────────────────────────────────────────╮ 84 | │ --remote -r If this option is used, │ 85 | │ all filenames are treated │ 86 | │ as paths to notebooks on │ 87 | │ your Databricks host (i.e. │ 88 | │ not local files). │ 89 | │ --profile -p NAME If using --remote, which │ 90 | │ Databricks profile to use. │ 91 | │ [default: DEFAULT] │ 92 | │ --line-length INTEGER How many characters per │ 93 | │ line to allow. │ 94 | │ [default: 88] │ 95 | │ --sql-upper --no-sql-upper SQL keywords should be │ 96 | │ UPPERCASE or lowercase. │ 97 | │ [default: sql-upper] │ 98 | │ --check Don't write the files │ 99 | │ back, just return the │ 100 | │ status. Return code 0 │ 101 | │ means nothing would │ 102 | │ change. │ 103 | │ --diff Don't write the files │ 104 | │ back, just output a diff │ 105 | │ for each file on stdout. │ 106 | │ --version Display version │ 107 | │ information and exit. │ 108 | │ --help Show this message and │ 109 | │ exit. │ 110 | ╰────────────────────────────────────────────────────────────────────────────────────────╯ 111 | ``` 112 | 113 | 114 | 115 | ## Version control integration 116 | 117 | Use [pre-commit](https://pre-commit.com). Add a `.pre-commit-config.yaml` file 118 | to your repo with the following content (changing/removing the `args` as you 119 | wish): 120 | 121 | ```yaml 122 | repos: 123 | - repo: https://github.com/inspera/blackbricks 124 | rev: 1.0.0 125 | hooks: 126 | - id: blackbricks 127 | args: [--line-length=120] 128 | ``` 129 | 130 | Set the `rev` attribute to the most recent version of `blackbricks`. 131 | The `args` are optional and can be used to set any of `blackbricks` options. 132 | 133 | ## Contributing 134 | 135 | If you find blackbricks useful, feel free to say so with a star. If you think it is utterly broken, you are more than 136 | welcome to contribute improvements. Please open an issue first to discuss what you want added/fixed. Unless you are just 137 | adding tests. In that case your pull request is extremely likely to be merged right away. 138 | 139 | ## FAQ 140 | 141 | ### Can I disable SQL formatting? 142 | 143 | Sure! Certain SQL statements might not be parsed and indented properly by `sqlparse`, and the result can be jumbled 144 | formatting. You can disable SQL formatting for a cell by adding `-- nofmt` to the very first line of a cell: 145 | 146 | ```sql 147 | %sql -- nofmt 148 | select this, 149 | sql_will, -- be kept just 150 | like_this 151 | from if_that_is.what_you_need 152 | ``` 153 | 154 | ### How do I use `blackbricks` on my Databricks notebooks? 155 | 156 | First, make sure you have set up `databricks-cli` on your system (see [installation](#installation)), and that you have 157 | at least one profile setup in `~/.databrickscfg`. As an example: 158 | 159 | ```cfg 160 | # File: ~/.databrickscfg 161 | 162 | [DEFAULT] 163 | host = https://dbc-b23456-a1243.cloud.databricks.com/ 164 | username = username@example.com 165 | password = dapi12345678901234567890 166 | 167 | [OTHERPROFILE] 168 | host = https://dbc-c54321-d234.cloud.databricks.com 169 | username = name.user@example.com 170 | password = dapi09876543211234567890 171 | ``` 172 | 173 | You should use [access tokens](https://docs.databricks.com/dev-tools/api/latest/authentication.html) instead of your actual password. 174 | 175 | You can then do: 176 | 177 | ``` bash 178 | $ blackbricks --remote /Users/username@example.com/notebook.py # Uses DEFAULT profile. 179 | $ blackbricks --remote notebook.py # Equivalent to the above. 180 | $ blackbricks --remote --profile OTHERPROFILE /Users/name.user@example.com/notebook.py 181 | $ blackbricks --remote --profile OTHERPROFILE notebook.py # Equivalent to the above. 182 | $ blackbricks --remote /Repos/username@example.com/repo-name/notebook.py # Targeting notebook in a Repo 183 | ``` 184 | 185 | ### Can you run blackbricks while using Databricks in the browser? 186 | 187 | No. See https://github.com/inspera/blackbricks/issues/27 for why. 188 | 189 | However, Databricks now allows you to [format your notebooks with black directly](https://docs.databricks.com/notebooks/notebooks-use.html#format-code-cells). 190 | 191 | ### I get an error: `TypeError: init() got an unexpected keyword argument 'no_args_is_help'` 192 | 193 | This means you had an old version of `click` installed from before, and your installation didn't upgrade it 194 | automatically. Updating your installation should do the trick, e.g. `pip install -U blackbricks` or similar depending on 195 | your installation method of choice. 196 | 197 | 198 | ### Shell commands like `!ls` throws an error 199 | 200 | See https://github.com/inspera/blackbricks/issues/21. 201 | 202 | ## Breaking changes 203 | 204 | ### Version policy 205 | 206 | Style choices made by `blackbricks` will follow semantic versioning, with changes that cause differences resulting in 207 | new major versions. Such changes will be kept to an absolute minimum, with none currently planned. 208 | 209 | Style choices made by `black` (responsible for 95% of the formatting in a notebook) will not follow the same strict 210 | semantic versioning. This is because `black` itself does not use semver, but instead provide a [year-based 211 | policy](https://black.readthedocs.io/en/stable/the_black_code_style/index.html#stability-policy). `blackbricks` will 212 | make a _minor_ version increase when it upgrades black to a new year. Such a bump should be made once the new year's 213 | release of `black` is available. Feel free to open an issue if this has not been done yet. 214 | 215 | ### Breaking changes with version 2.0 216 | 217 | Notebooks will be terminated with a `\n` starting with version `2.0.0`. This harmonizes EOF handling and should be much 218 | less annoying in practice than prior versions. This causes a diff on _any_ notebook that was previously formatted with 219 | `blackbricks<2.0.0`. 220 | 221 | Also, the deprecated and non-functional flag for two space indentation is removed, and providing said flag is now an error. 222 | 223 | ### Breaking changes with version 1.0 224 | 225 | Earlier versions of blackbricks applied a patched version of black in order to allow two-space indentation. This was 226 | done because Databricks used two-space indentation, and did not allow you to change that. 227 | 228 | Since then, Databricks has added the option to choose. Because you can now choose, blackbricks re-joins black in being 229 | uncompromising, and since version 1.0 you can no longer choose anything but 4 space indentation. 230 | 231 | If you _must_ keep using two-space indentation, then stick to versions `<1.0`. 232 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "argcomplete" 5 | version = "2.0.6" 6 | description = "Bash tab completion for argparse" 7 | optional = false 8 | python-versions = ">=3.6" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "argcomplete-2.0.6-py3-none-any.whl", hash = "sha256:6c2170b3e0ab54683cb28d319b65261bde1f11388be688b68118b7d281e34c94"}, 12 | {file = "argcomplete-2.0.6.tar.gz", hash = "sha256:dc33528d96727882b576b24bc89ed038f3c6abbb6855ff9bb6be23384afff9d6"}, 13 | ] 14 | 15 | [package.extras] 16 | lint = ["flake8", "mypy"] 17 | test = ["coverage", "flake8", "mypy", "pexpect", "wheel"] 18 | 19 | [[package]] 20 | name = "black" 21 | version = "24.4.2" 22 | description = "The uncompromising code formatter." 23 | optional = false 24 | python-versions = ">=3.8" 25 | groups = ["main"] 26 | files = [ 27 | {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, 28 | {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, 29 | {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, 30 | {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, 31 | {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, 32 | {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, 33 | {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, 34 | {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, 35 | {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, 36 | {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, 37 | {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, 38 | {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, 39 | {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, 40 | {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, 41 | {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, 42 | {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, 43 | {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, 44 | {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, 45 | {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, 46 | {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, 47 | {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, 48 | {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, 49 | ] 50 | 51 | [package.dependencies] 52 | click = ">=8.0.0" 53 | mypy-extensions = ">=0.4.3" 54 | packaging = ">=22.0" 55 | pathspec = ">=0.9.0" 56 | platformdirs = ">=2" 57 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 58 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 59 | 60 | [package.extras] 61 | colorama = ["colorama (>=0.4.3)"] 62 | d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] 63 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 64 | uvloop = ["uvloop (>=0.15.2)"] 65 | 66 | [[package]] 67 | name = "certifi" 68 | version = "2024.7.4" 69 | description = "Python package for providing Mozilla's CA Bundle." 70 | optional = false 71 | python-versions = ">=3.6" 72 | groups = ["main"] 73 | files = [ 74 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 75 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 76 | ] 77 | 78 | [[package]] 79 | name = "charset-normalizer" 80 | version = "2.1.1" 81 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 82 | optional = false 83 | python-versions = ">=3.6.0" 84 | groups = ["main", "dev"] 85 | files = [ 86 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 87 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 88 | ] 89 | 90 | [package.extras] 91 | unicode-backport = ["unicodedata2"] 92 | 93 | [[package]] 94 | name = "click" 95 | version = "8.1.7" 96 | description = "Composable command line interface toolkit" 97 | optional = false 98 | python-versions = ">=3.7" 99 | groups = ["main"] 100 | files = [ 101 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 102 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 103 | ] 104 | 105 | [package.dependencies] 106 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 107 | 108 | [[package]] 109 | name = "colorama" 110 | version = "0.4.6" 111 | description = "Cross-platform colored terminal text." 112 | optional = false 113 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 114 | groups = ["main", "dev"] 115 | files = [ 116 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 117 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 118 | ] 119 | 120 | [[package]] 121 | name = "commitizen" 122 | version = "2.42.1" 123 | description = "Python commitizen client tool" 124 | optional = false 125 | python-versions = ">=3.6.2,<4.0.0" 126 | groups = ["dev"] 127 | files = [ 128 | {file = "commitizen-2.42.1-py3-none-any.whl", hash = "sha256:fad7d37cfae361a859b713d4ac591859d5ca03137dd52de4e1bd208f7f45d5dc"}, 129 | {file = "commitizen-2.42.1.tar.gz", hash = "sha256:eac18c7c65587061aac6829534907aeb208405b8230bfd35ec08503c228a7f17"}, 130 | ] 131 | 132 | [package.dependencies] 133 | argcomplete = ">=1.12.1,<2.1" 134 | charset-normalizer = ">=2.1.0,<3.0.0" 135 | colorama = ">=0.4.1,<0.5.0" 136 | decli = ">=0.5.2,<0.6.0" 137 | jinja2 = ">=2.10.3" 138 | packaging = ">=19" 139 | pyyaml = ">=3.08" 140 | questionary = ">=1.4.0,<2.0.0" 141 | termcolor = {version = ">=1.1,<3", markers = "python_version >= \"3.7\""} 142 | tomlkit = ">=0.5.3,<1.0.0" 143 | typing-extensions = ">=4.0.1,<5.0.0" 144 | 145 | [[package]] 146 | name = "commonmark" 147 | version = "0.9.1" 148 | description = "Python parser for the CommonMark Markdown spec" 149 | optional = false 150 | python-versions = "*" 151 | groups = ["main"] 152 | files = [ 153 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 154 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 155 | ] 156 | 157 | [package.extras] 158 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 159 | 160 | [[package]] 161 | name = "databricks-cli" 162 | version = "0.17.8" 163 | description = "A command line interface for Databricks" 164 | optional = false 165 | python-versions = "*" 166 | groups = ["main"] 167 | files = [ 168 | {file = "databricks-cli-0.17.8.tar.gz", hash = "sha256:1d5ef6520ea3ed403d58f24538641bd8eeb163621f5054205fcf643578698579"}, 169 | {file = "databricks_cli-0.17.8-py2-none-any.whl", hash = "sha256:dfb7d3aeec17489b6051789bf7321694d875ea487be8652f8f58fabceead7d9e"}, 170 | ] 171 | 172 | [package.dependencies] 173 | click = ">=7.0" 174 | oauthlib = ">=3.1.0" 175 | pyjwt = ">=1.7.0" 176 | requests = ">=2.17.3" 177 | six = ">=1.10.0" 178 | tabulate = ">=0.7.7" 179 | urllib3 = ">=1.26.7,<2.0.0" 180 | 181 | [[package]] 182 | name = "decli" 183 | version = "0.5.2" 184 | description = "Minimal, easy-to-use, declarative cli tool" 185 | optional = false 186 | python-versions = ">=3.6" 187 | groups = ["dev"] 188 | files = [ 189 | {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, 190 | {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, 191 | ] 192 | 193 | [[package]] 194 | name = "exceptiongroup" 195 | version = "1.2.2" 196 | description = "Backport of PEP 654 (exception groups)" 197 | optional = false 198 | python-versions = ">=3.7" 199 | groups = ["dev"] 200 | markers = "python_version < \"3.11\"" 201 | files = [ 202 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 203 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 204 | ] 205 | 206 | [package.extras] 207 | test = ["pytest (>=6)"] 208 | 209 | [[package]] 210 | name = "flake8" 211 | version = "3.9.2" 212 | description = "the modular source code checker: pep8 pyflakes and co" 213 | optional = false 214 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 215 | groups = ["dev"] 216 | files = [ 217 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 218 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 219 | ] 220 | 221 | [package.dependencies] 222 | mccabe = ">=0.6.0,<0.7.0" 223 | pycodestyle = ">=2.7.0,<2.8.0" 224 | pyflakes = ">=2.3.0,<2.4.0" 225 | 226 | [[package]] 227 | name = "idna" 228 | version = "3.7" 229 | description = "Internationalized Domain Names in Applications (IDNA)" 230 | optional = false 231 | python-versions = ">=3.5" 232 | groups = ["main"] 233 | files = [ 234 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 235 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 236 | ] 237 | 238 | [[package]] 239 | name = "iniconfig" 240 | version = "2.0.0" 241 | description = "brain-dead simple config-ini parsing" 242 | optional = false 243 | python-versions = ">=3.7" 244 | groups = ["dev"] 245 | files = [ 246 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 247 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 248 | ] 249 | 250 | [[package]] 251 | name = "isort" 252 | version = "5.13.2" 253 | description = "A Python utility / library to sort Python imports." 254 | optional = false 255 | python-versions = ">=3.8.0" 256 | groups = ["dev"] 257 | files = [ 258 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 259 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 260 | ] 261 | 262 | [package.extras] 263 | colors = ["colorama (>=0.4.6)"] 264 | 265 | [[package]] 266 | name = "jinja2" 267 | version = "3.1.6" 268 | description = "A very fast and expressive template engine." 269 | optional = false 270 | python-versions = ">=3.7" 271 | groups = ["dev"] 272 | files = [ 273 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 274 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 275 | ] 276 | 277 | [package.dependencies] 278 | MarkupSafe = ">=2.0" 279 | 280 | [package.extras] 281 | i18n = ["Babel (>=2.7)"] 282 | 283 | [[package]] 284 | name = "markupsafe" 285 | version = "2.1.5" 286 | description = "Safely add untrusted strings to HTML/XML markup." 287 | optional = false 288 | python-versions = ">=3.7" 289 | groups = ["dev"] 290 | files = [ 291 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 292 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 293 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 294 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 295 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 296 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 297 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 298 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 299 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 300 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 301 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 302 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 303 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 304 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 305 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 306 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 307 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 308 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 309 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 310 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 311 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 312 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 313 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 314 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 315 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 316 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 317 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 318 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 319 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 320 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 321 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 322 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 323 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 324 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 325 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 326 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 327 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 328 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 329 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 330 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 331 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 332 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 333 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 334 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 335 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 336 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 337 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 338 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 339 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 340 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 341 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 342 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 343 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 344 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 345 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 346 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 347 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 348 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 349 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 350 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 351 | ] 352 | 353 | [[package]] 354 | name = "mccabe" 355 | version = "0.6.1" 356 | description = "McCabe checker, plugin for flake8" 357 | optional = false 358 | python-versions = "*" 359 | groups = ["dev"] 360 | files = [ 361 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 362 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 363 | ] 364 | 365 | [[package]] 366 | name = "mypy" 367 | version = "0.971" 368 | description = "Optional static typing for Python" 369 | optional = false 370 | python-versions = ">=3.6" 371 | groups = ["dev"] 372 | files = [ 373 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, 374 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, 375 | {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, 376 | {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"}, 377 | {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, 378 | {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, 379 | {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"}, 380 | {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, 381 | {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, 382 | {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"}, 383 | {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, 384 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, 385 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, 386 | {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, 387 | {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"}, 388 | {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, 389 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, 390 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, 391 | {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, 392 | {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"}, 393 | {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, 394 | {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, 395 | {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, 396 | ] 397 | 398 | [package.dependencies] 399 | mypy-extensions = ">=0.4.3" 400 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 401 | typing-extensions = ">=3.10" 402 | 403 | [package.extras] 404 | dmypy = ["psutil (>=4.0)"] 405 | python2 = ["typed-ast (>=1.4.0,<2)"] 406 | reports = ["lxml"] 407 | 408 | [[package]] 409 | name = "mypy-extensions" 410 | version = "1.0.0" 411 | description = "Type system extensions for programs checked with the mypy type checker." 412 | optional = false 413 | python-versions = ">=3.5" 414 | groups = ["main", "dev"] 415 | files = [ 416 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 417 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 418 | ] 419 | 420 | [[package]] 421 | name = "oauthlib" 422 | version = "3.2.2" 423 | description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" 424 | optional = false 425 | python-versions = ">=3.6" 426 | groups = ["main"] 427 | files = [ 428 | {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, 429 | {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, 430 | ] 431 | 432 | [package.extras] 433 | rsa = ["cryptography (>=3.0.0)"] 434 | signals = ["blinker (>=1.4.0)"] 435 | signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] 436 | 437 | [[package]] 438 | name = "packaging" 439 | version = "24.1" 440 | description = "Core utilities for Python packages" 441 | optional = false 442 | python-versions = ">=3.8" 443 | groups = ["main", "dev"] 444 | files = [ 445 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 446 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 447 | ] 448 | 449 | [[package]] 450 | name = "pathspec" 451 | version = "0.12.1" 452 | description = "Utility library for gitignore style pattern matching of file paths." 453 | optional = false 454 | python-versions = ">=3.8" 455 | groups = ["main"] 456 | files = [ 457 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 458 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 459 | ] 460 | 461 | [[package]] 462 | name = "platformdirs" 463 | version = "4.2.2" 464 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 465 | optional = false 466 | python-versions = ">=3.8" 467 | groups = ["main"] 468 | files = [ 469 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 470 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 471 | ] 472 | 473 | [package.extras] 474 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 475 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 476 | type = ["mypy (>=1.8)"] 477 | 478 | [[package]] 479 | name = "pluggy" 480 | version = "1.5.0" 481 | description = "plugin and hook calling mechanisms for python" 482 | optional = false 483 | python-versions = ">=3.8" 484 | groups = ["dev"] 485 | files = [ 486 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 487 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 488 | ] 489 | 490 | [package.extras] 491 | dev = ["pre-commit", "tox"] 492 | testing = ["pytest", "pytest-benchmark"] 493 | 494 | [[package]] 495 | name = "prompt-toolkit" 496 | version = "3.0.47" 497 | description = "Library for building powerful interactive command lines in Python" 498 | optional = false 499 | python-versions = ">=3.7.0" 500 | groups = ["dev"] 501 | files = [ 502 | {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, 503 | {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, 504 | ] 505 | 506 | [package.dependencies] 507 | wcwidth = "*" 508 | 509 | [[package]] 510 | name = "pycodestyle" 511 | version = "2.7.0" 512 | description = "Python style guide checker" 513 | optional = false 514 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 515 | groups = ["dev"] 516 | files = [ 517 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 518 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 519 | ] 520 | 521 | [[package]] 522 | name = "pyflakes" 523 | version = "2.3.1" 524 | description = "passive checker of Python programs" 525 | optional = false 526 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 527 | groups = ["dev"] 528 | files = [ 529 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 530 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 531 | ] 532 | 533 | [[package]] 534 | name = "pygments" 535 | version = "2.18.0" 536 | description = "Pygments is a syntax highlighting package written in Python." 537 | optional = false 538 | python-versions = ">=3.8" 539 | groups = ["main"] 540 | files = [ 541 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 542 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 543 | ] 544 | 545 | [package.extras] 546 | windows-terminal = ["colorama (>=0.4.6)"] 547 | 548 | [[package]] 549 | name = "pyjwt" 550 | version = "2.9.0" 551 | description = "JSON Web Token implementation in Python" 552 | optional = false 553 | python-versions = ">=3.8" 554 | groups = ["main"] 555 | files = [ 556 | {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, 557 | {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, 558 | ] 559 | 560 | [package.extras] 561 | crypto = ["cryptography (>=3.4.0)"] 562 | dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] 563 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 564 | tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] 565 | 566 | [[package]] 567 | name = "pytest" 568 | version = "7.4.4" 569 | description = "pytest: simple powerful testing with Python" 570 | optional = false 571 | python-versions = ">=3.7" 572 | groups = ["dev"] 573 | files = [ 574 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 575 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 576 | ] 577 | 578 | [package.dependencies] 579 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 580 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 581 | iniconfig = "*" 582 | packaging = "*" 583 | pluggy = ">=0.12,<2.0" 584 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 585 | 586 | [package.extras] 587 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 588 | 589 | [[package]] 590 | name = "pyyaml" 591 | version = "6.0.1" 592 | description = "YAML parser and emitter for Python" 593 | optional = false 594 | python-versions = ">=3.6" 595 | groups = ["dev"] 596 | files = [ 597 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 598 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 599 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 600 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 601 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 602 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 603 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 604 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 605 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 606 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 607 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 608 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 609 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 610 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 611 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 612 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 613 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 614 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 615 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 616 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 617 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 618 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 619 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 620 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 621 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 622 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 623 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 624 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 625 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 626 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 627 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 628 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 629 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 630 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 631 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 632 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 633 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 634 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 635 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 636 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 637 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 638 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 639 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 640 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 641 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 642 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 643 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 644 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 645 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 646 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 647 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 648 | ] 649 | 650 | [[package]] 651 | name = "questionary" 652 | version = "1.10.0" 653 | description = "Python library to build pretty command line user prompts ⭐️" 654 | optional = false 655 | python-versions = ">=3.6,<4.0" 656 | groups = ["dev"] 657 | files = [ 658 | {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, 659 | {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, 660 | ] 661 | 662 | [package.dependencies] 663 | prompt_toolkit = ">=2.0,<4.0" 664 | 665 | [package.extras] 666 | docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] 667 | 668 | [[package]] 669 | name = "requests" 670 | version = "2.32.4" 671 | description = "Python HTTP for Humans." 672 | optional = false 673 | python-versions = ">=3.8" 674 | groups = ["main"] 675 | files = [ 676 | {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, 677 | {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, 678 | ] 679 | 680 | [package.dependencies] 681 | certifi = ">=2017.4.17" 682 | charset_normalizer = ">=2,<4" 683 | idna = ">=2.5,<4" 684 | urllib3 = ">=1.21.1,<3" 685 | 686 | [package.extras] 687 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 688 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 689 | 690 | [[package]] 691 | name = "rich" 692 | version = "12.6.0" 693 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 694 | optional = false 695 | python-versions = ">=3.6.3,<4.0.0" 696 | groups = ["main"] 697 | files = [ 698 | {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, 699 | {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, 700 | ] 701 | 702 | [package.dependencies] 703 | commonmark = ">=0.9.0,<0.10.0" 704 | pygments = ">=2.6.0,<3.0.0" 705 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} 706 | 707 | [package.extras] 708 | jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] 709 | 710 | [[package]] 711 | name = "shellingham" 712 | version = "1.5.4" 713 | description = "Tool to Detect Surrounding Shell" 714 | optional = false 715 | python-versions = ">=3.7" 716 | groups = ["main"] 717 | files = [ 718 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 719 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 720 | ] 721 | 722 | [[package]] 723 | name = "six" 724 | version = "1.16.0" 725 | description = "Python 2 and 3 compatibility utilities" 726 | optional = false 727 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 728 | groups = ["main"] 729 | files = [ 730 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 731 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 732 | ] 733 | 734 | [[package]] 735 | name = "sqlparse" 736 | version = "0.4.4" 737 | description = "A non-validating SQL parser." 738 | optional = false 739 | python-versions = ">=3.5" 740 | groups = ["main"] 741 | files = [ 742 | {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, 743 | {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, 744 | ] 745 | 746 | [package.extras] 747 | dev = ["build", "flake8"] 748 | doc = ["sphinx"] 749 | test = ["pytest", "pytest-cov"] 750 | 751 | [[package]] 752 | name = "tabulate" 753 | version = "0.9.0" 754 | description = "Pretty-print tabular data" 755 | optional = false 756 | python-versions = ">=3.7" 757 | groups = ["main"] 758 | files = [ 759 | {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, 760 | {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, 761 | ] 762 | 763 | [package.extras] 764 | widechars = ["wcwidth"] 765 | 766 | [[package]] 767 | name = "termcolor" 768 | version = "2.4.0" 769 | description = "ANSI color formatting for output in terminal" 770 | optional = false 771 | python-versions = ">=3.8" 772 | groups = ["dev"] 773 | files = [ 774 | {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, 775 | {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, 776 | ] 777 | 778 | [package.extras] 779 | tests = ["pytest", "pytest-cov"] 780 | 781 | [[package]] 782 | name = "tomli" 783 | version = "2.0.1" 784 | description = "A lil' TOML parser" 785 | optional = false 786 | python-versions = ">=3.7" 787 | groups = ["main", "dev"] 788 | markers = "python_version < \"3.11\"" 789 | files = [ 790 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 791 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 792 | ] 793 | 794 | [[package]] 795 | name = "tomlkit" 796 | version = "0.13.0" 797 | description = "Style preserving TOML library" 798 | optional = false 799 | python-versions = ">=3.8" 800 | groups = ["dev"] 801 | files = [ 802 | {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, 803 | {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, 804 | ] 805 | 806 | [[package]] 807 | name = "typer" 808 | version = "0.7.0" 809 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 810 | optional = false 811 | python-versions = ">=3.6" 812 | groups = ["main"] 813 | files = [ 814 | {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"}, 815 | {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"}, 816 | ] 817 | 818 | [package.dependencies] 819 | click = ">=7.1.1,<9.0.0" 820 | colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} 821 | rich = {version = ">=10.11.0,<13.0.0", optional = true, markers = "extra == \"all\""} 822 | shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} 823 | 824 | [package.extras] 825 | all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 826 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] 827 | 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)"] 828 | 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)"] 829 | 830 | [[package]] 831 | name = "typing-extensions" 832 | version = "4.12.2" 833 | description = "Backported and Experimental Type Hints for Python 3.8+" 834 | optional = false 835 | python-versions = ">=3.8" 836 | groups = ["main", "dev"] 837 | files = [ 838 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 839 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 840 | ] 841 | markers = {main = "python_version < \"3.11\""} 842 | 843 | [[package]] 844 | name = "urllib3" 845 | version = "1.26.19" 846 | description = "HTTP library with thread-safe connection pooling, file post, and more." 847 | optional = false 848 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 849 | groups = ["main"] 850 | files = [ 851 | {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, 852 | {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, 853 | ] 854 | 855 | [package.extras] 856 | brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] 857 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 858 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 859 | 860 | [[package]] 861 | name = "wcwidth" 862 | version = "0.2.13" 863 | description = "Measures the displayed width of unicode strings in a terminal" 864 | optional = false 865 | python-versions = "*" 866 | groups = ["dev"] 867 | files = [ 868 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 869 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 870 | ] 871 | 872 | [metadata] 873 | lock-version = "2.1" 874 | python-versions = "^3.8" 875 | content-hash = "97c8d9f50ca60e9c89624fe1474b493f2f21407754a2281ed2b636cedbd06e95" 876 | --------------------------------------------------------------------------------