├── .gitignore ├── .pre-commit-config.yaml ├── CODEOWNERS ├── LICENSE ├── README.md ├── poetry.lock ├── pyproject.toml ├── setup.py └── src └── optician ├── __main__.py ├── cli ├── __init__.py └── commands.py ├── db_client ├── __init__.py └── db_client.py ├── diff_tracker ├── __init__.py └── diff_tracker.py ├── logger.py ├── lookml_generator ├── __init__.py └── lookml_generator.py └── vc_client ├── __init__.py └── vc_client.py /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 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 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # MacBook 156 | .DS_Store 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ 164 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 23.7.0 4 | hooks: 5 | - id: black 6 | language_version: python3.9 7 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @getground/business-intelligence @getground/sre 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 GetGround 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optician 2 | Optician automatically generates your LookML views from your database models. 3 | 4 | Optician was created at [GetGround](https://www.getground.co.uk/), where we use dbt, BigQuery and Looker, so you may realise that its usage may be aligned with this data stack paradigm. 5 | 6 | Optician has 3 features: 7 | - LookML generator: this is the main feature, that reads the table schemas from database and creates the LookML base views 8 | - [Optional] Track differences of models between tables in two distinct schemas in the same database, which are meant to correspond to your development and production schemas, in order to identify which models need to be synced. 9 | - [Optional] Commit the LookML view files to your Looker repository (only supports GitHub at the moment) 10 | 11 | Assumptions: 12 | - The fields in your tables have descriptions (supported by some databases only). If you use dbt, you can use `persist-docs` to store the fields descriptions in the database (see [dbt docs](https://docs.getdbt.com/reference/resource-configs/persist_docs)). 13 | - Your Looker project is structured with base, standard and logical layers, following [this approach](https://www.spectacles.dev/post/fix-your-lookml-project-structure). Optician helps you create and update your base layer, without any measures. 14 | 15 | ## Installation 16 | 17 | You can install optician from a PyPi repository. We suggest you install it into a virtual environment. 18 | Please specify which database (or databases) you will be using, as in the example below. 19 | 20 | ```shell 21 | pip install "optician[bigquery]" 22 | ``` 23 | 24 | ## Setup 25 | 1. Create the Optician configuration file `.optician/config.json` (you can name it another way) somewhere in your computer (we suggest inside the dbt or Looker repo) 26 | 2. Create the environment variable `OPTICIAN_CONFIG_FILE` which will be the absolute path to the file created in 1 27 | 3. You need to be able to connect to your database. For BigQuery, you can connect either by Oauth or Service Account. 28 | 4. [Optional] Create an environment variable `GH_TOKEN` for your GitHub personal token. You need to create this token in [GitHub](https://github.com/settings/tokens) with read:project, repo, user:email permissions. This will allow you to commit your Looker views directly to the Looker repository. 29 | 30 | ### Config file 31 | 32 | The config file allows you to customise your LookML views. It supports the following options: 33 | 34 | - `hide_all_fields`: Defaults to false. If set to true, all fields in the LookML view will be hidden in Looker. 35 | 36 | - `timeframes`: The list of timeframes to use for your time dimension group fields. It uses the following timeframes as default: 37 | ```json 38 | "timeframes": [ 39 | "raw", 40 | "time", 41 | "date", 42 | "week", 43 | "month", 44 | "month_name", 45 | "month_num", 46 | "quarter", 47 | "quarter_of_year", 48 | "year" 49 | ] 50 | ``` 51 | 52 | - `capitalize_ids`: Defaults to True. If your field name contains "Id", it will be replaced by "ID". You can set it to false if it's messing up with any other field names. 53 | 54 | - `primary_key_columns`: List of field names to be assumed to be a primary key in Looker. E.g: ` "primary_key_columns": ["pk", "id", "primary_key"]`. 55 | 56 | - `ignore_column_types`: List of database field types to be ignored on the LookML creation. E.g: `"ignore_column_types": ["GEOGRAPHY", "ARRAY"]` 57 | 58 | - `ignore_modes`: List of database field modes that will be ignored on the LookML creation. E.g: `"ignore_modes": ["REPEATED"]` 59 | 60 | - `time_suffixes`: List of suffixes on your database field names to be ommitted in the Looker field names. E.g. `"time_suffixes": ["_at", "_date", "_time", "_ts", "_timestamp", "_datetime"]` will mean that the field `created_at` will be named `created` in Looker. 61 | 62 | - `order_by`: Defaults to `alpha`. How to order the fields in the LookML view. Use `alpha` for alphabetical order or `table` to use the same order as in your database table. 63 | 64 | Example of a config file: 65 | 66 | ```json 67 | { 68 | "primary_key_columns": ["id", "pk", "primary_key"], 69 | "ignore_column_types": ["GEOGRAPHY", "ARRAY"], 70 | "ignore_modes": ["REPEATED"], 71 | "timeframes":[ 72 | "raw", 73 | "time", 74 | "date", 75 | "week", 76 | "month", 77 | "month_name", 78 | "month_num", 79 | "quarter", 80 | "quarter_of_year", 81 | "year" 82 | ], 83 | "hide_all_fields": false, 84 | "capitalize_ids": true, 85 | "time_suffixes": ["_at", "_date", "_time", "_ts", "_timestamp", "_datetime"], 86 | "order_by": "alpha" 87 | } 88 | ``` 89 | 90 | 91 | ## Commands 92 | 93 | ### Diff Tracker 94 | 95 | ```bash 96 | optician diff_tracker [options] 97 | ``` 98 | 99 | Use this command if you need to compare models between 2 different schemas in a database, supposed to correspond to your dev and prod targets. This command will return a list of the models which are different, ie, the ones you will need to update. This can be particulary useful when developping on your models, to know which models you will need to refresh in Looker. 100 | 101 | #### Arguments 102 | 103 | - `--db_type` (type: str, required: True): Database type (bigquery, redshift, snowflake). 104 | 105 | - `--dataset1_name` (type: str, required: True): Name of Dataset 1. 106 | 107 | - `--dataset2_name` (type: str, required: True): Name of Dataset 2. 108 | 109 | - `--project` (type: str, required: True): Project ID. 110 | 111 | - `--service_account` (type: str, required: False): Google Service Account. 112 | 113 | - `--models` (type: str): List of model names to compare (comma-separated) or file path with a model per line. You can pass your dbt marts only, for example. 114 | 115 | - `--output` (type: str): Output file path to write the results to. 116 | 117 | - `--full-refresh` (action: Boolean, default: False): If you want to perform a full refresh of all models. This will return all models inputed (so this step does not run). This is only useful if you want to skip this command when refreshing all models, for example in a CI pipeline. 118 | 119 | #### Example 120 | ```bash 121 | optician diff_tracker \ 122 | --db_type bigquery \ 123 | --project my-database-name \ 124 | --dataset1_name dbt_dev \ 125 | --dataset2_name dbt_prod \ 126 | --models tmp/marts.txt \ 127 | --output tmp/diff.txt 128 | ``` 129 | 130 | ### Generate LookML 131 | 132 | ```bash 133 | optician generate_lookml [options] 134 | ``` 135 | 136 | This command created the LookML base views. 137 | 138 | #### Arguments 139 | 140 | - `--db_type` (type: str, required: True): Database type (bigquery, redshift, snowflake). 141 | 142 | - `--project` (type: str, required: True): Project ID. 143 | 144 | - `--dataset` (type: str, required: True): Dataset ID/database schema to read the models from. 145 | 146 | - `--tables` (type: str, required: True): List of Table IDs separated by a comma or provide a file path with a table name per line. You can use the same file outputted by the `diff_tracker`, for example. 147 | 148 | - `--output-dir` (type: str, required: False): Output Directory. If not specified, it will write the files to the current directory. 149 | 150 | - `--override-dataset-id` (type: str, required: False): Override Dataset ID. For example, you may be developping and reading from a dev dataset, but you may want to create the LookML views pointing to your production dataset. Or you may have defined a constant in Looker for you dataset name 151 | ```lookml 152 | constant: dataset { 153 | value: "dbt_prod" 154 | } 155 | ``` 156 | and set this option value to @{dataset} as in Example 2. 157 | 158 | - `--service-account` (type: str, required: False): Service Account. 159 | 160 | #### Examples 161 | 162 | Example 1: 163 | ```bash 164 | optician generate_lookml \ 165 | --db_type bigquery \ 166 | --project my-database-name \ 167 | --dataset dbt_dev \ 168 | --tables tmp/diff.txt \ 169 | --override-dataset-id dbt_prod \ 170 | --output tmp/lookml/ 171 | ``` 172 | 173 | Example 2: 174 | ```bash 175 | optician generate_lookml \ 176 | --db_type snowflake \ 177 | --project my-database-name \ 178 | --dataset dbt_dev \ 179 | --tables deals,contacts \ 180 | --override-dataset-id @{dataset} 181 | ``` 182 | 183 | ### Push to Looker 184 | 185 | ```bash 186 | optician push_to_looker [options] 187 | ``` 188 | 189 | You can use this command to commit and push your LookML views generated to your Looker GitHub repository. 190 | 191 | #### Arguments 192 | 193 | - `--token` (type: str, required: True): GitHub Token. 194 | 195 | - `--repo` (type: str, required: True): GitHub Repo. 196 | 197 | - `--user-email` (type: str, required: False): GitHub User Email. 198 | 199 | - `--input-dir` (type: str, required: True): Path that contains new files to be committed. 200 | 201 | - `--output-dir` (type: str, required: True): Directory in the repo to write the LookML files to. 202 | 203 | - `--branch-name` (type: str, required: True): Name of the branch to commit the changes to. 204 | 205 | - `--base-branch` (type: str, default: "main"): Name of the base branch (e.g. main, master). 206 | 207 | #### Examples 208 | 209 | Example 1: 210 | In this example we have the GitHub Token saved in an environment variable `$GH_TOKEN`. We are reading the views from the directory tmp/lookml that we have used as output in the `generate_lookml` command and we are writing the files to a folder _base in our repo. 211 | 212 | ```bash 213 | optician push_to_looker \ 214 | --token $GH_TOKEN \ 215 | --repo mycompany/looker \ 216 | --branch-name update-deals \ 217 | --base-branch main \ 218 | --input-dir tmp/lookml/ \ 219 | --output-dir _base 220 | ``` 221 | 222 | ## How to contribute 223 | 224 | We are only supporting BigQuery at the moment, but you are able to contribute by updating the `db_client.py` file. 225 | 226 | In order to contribute, fork this repository, develop on a new branch and then open a pull request. 227 | 228 | Make sure you install all dependencies into a virtual environment and also install pre-commit `pre-commit install` so that the code is linted when committing. 229 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asn1crypto" 5 | version = "1.5.1" 6 | description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" 7 | optional = true 8 | python-versions = "*" 9 | files = [ 10 | {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, 11 | {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, 12 | ] 13 | 14 | [[package]] 15 | name = "cachetools" 16 | version = "5.3.3" 17 | description = "Extensible memoizing collections and decorators" 18 | optional = true 19 | python-versions = ">=3.7" 20 | files = [ 21 | {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, 22 | {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, 23 | ] 24 | 25 | [[package]] 26 | name = "certifi" 27 | version = "2024.2.2" 28 | description = "Python package for providing Mozilla's CA Bundle." 29 | optional = false 30 | python-versions = ">=3.6" 31 | files = [ 32 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 33 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 34 | ] 35 | 36 | [[package]] 37 | name = "cffi" 38 | version = "1.16.0" 39 | description = "Foreign Function Interface for Python calling C code." 40 | optional = false 41 | python-versions = ">=3.8" 42 | files = [ 43 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 44 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 45 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 46 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 47 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 48 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 49 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 50 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 51 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 52 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 53 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 54 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 55 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 56 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 57 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 58 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 59 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 60 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 61 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 62 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 63 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 64 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 65 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 66 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 67 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 68 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 69 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 70 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 71 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 72 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 73 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 74 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 75 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 76 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 77 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 78 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 79 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 80 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 81 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 82 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 83 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 84 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 85 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 86 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 87 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 88 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 89 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 90 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 91 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 92 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 93 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 94 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 95 | ] 96 | 97 | [package.dependencies] 98 | pycparser = "*" 99 | 100 | [[package]] 101 | name = "charset-normalizer" 102 | version = "3.3.2" 103 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 104 | optional = false 105 | python-versions = ">=3.7.0" 106 | files = [ 107 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 108 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 109 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 110 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 111 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 112 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 113 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 114 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 115 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 116 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 117 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 118 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 119 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 120 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 121 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 122 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 123 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 124 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 125 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 126 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 127 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 128 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 129 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 130 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 131 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 132 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 133 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 134 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 135 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 136 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 137 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 138 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 139 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 140 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 141 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 142 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 143 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 144 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 145 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 146 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 147 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 148 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 149 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 150 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 151 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 152 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 153 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 154 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 155 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 156 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 157 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 158 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 159 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 160 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 161 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 162 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 163 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 164 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 165 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 166 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 167 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 168 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 169 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 170 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 171 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 172 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 173 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 174 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 175 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 176 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 177 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 178 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 179 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 180 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 181 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 182 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 183 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 184 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 185 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 186 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 187 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 188 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 189 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 190 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 191 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 192 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 193 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 194 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 195 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 196 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 197 | ] 198 | 199 | [[package]] 200 | name = "cryptography" 201 | version = "42.0.7" 202 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 203 | optional = false 204 | python-versions = ">=3.7" 205 | files = [ 206 | {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"}, 207 | {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"}, 208 | {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"}, 209 | {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"}, 210 | {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"}, 211 | {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"}, 212 | {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"}, 213 | {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"}, 214 | {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"}, 215 | {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"}, 216 | {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"}, 217 | {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"}, 218 | {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"}, 219 | {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"}, 220 | {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"}, 221 | {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"}, 222 | {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"}, 223 | {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"}, 224 | {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"}, 225 | {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"}, 226 | {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, 227 | {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, 228 | {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, 229 | {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, 230 | {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, 231 | {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, 232 | {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, 233 | {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, 234 | {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, 235 | {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, 236 | {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, 237 | {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, 238 | ] 239 | 240 | [package.dependencies] 241 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 242 | 243 | [package.extras] 244 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 245 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 246 | nox = ["nox"] 247 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 248 | sdist = ["build"] 249 | ssh = ["bcrypt (>=3.1.5)"] 250 | test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 251 | test-randomorder = ["pytest-randomly"] 252 | 253 | [[package]] 254 | name = "deprecated" 255 | version = "1.2.14" 256 | description = "Python @deprecated decorator to deprecate old python classes, functions or methods." 257 | optional = false 258 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 259 | files = [ 260 | {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, 261 | {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, 262 | ] 263 | 264 | [package.dependencies] 265 | wrapt = ">=1.10,<2" 266 | 267 | [package.extras] 268 | dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] 269 | 270 | [[package]] 271 | name = "filelock" 272 | version = "3.14.0" 273 | description = "A platform independent file lock." 274 | optional = true 275 | python-versions = ">=3.8" 276 | files = [ 277 | {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, 278 | {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, 279 | ] 280 | 281 | [package.extras] 282 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 283 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 284 | typing = ["typing-extensions (>=4.8)"] 285 | 286 | [[package]] 287 | name = "google-api-core" 288 | version = "2.19.0" 289 | description = "Google API client core library" 290 | optional = true 291 | python-versions = ">=3.7" 292 | files = [ 293 | {file = "google-api-core-2.19.0.tar.gz", hash = "sha256:cf1b7c2694047886d2af1128a03ae99e391108a08804f87cfd35970e49c9cd10"}, 294 | {file = "google_api_core-2.19.0-py3-none-any.whl", hash = "sha256:8661eec4078c35428fd3f69a2c7ee29e342896b70f01d1a1cbcb334372dd6251"}, 295 | ] 296 | 297 | [package.dependencies] 298 | google-auth = ">=2.14.1,<3.0.dev0" 299 | googleapis-common-protos = ">=1.56.2,<2.0.dev0" 300 | grpcio = [ 301 | {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, 302 | {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, 303 | ] 304 | grpcio-status = [ 305 | {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, 306 | {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, 307 | ] 308 | proto-plus = ">=1.22.3,<2.0.0dev" 309 | protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" 310 | requests = ">=2.18.0,<3.0.0.dev0" 311 | 312 | [package.extras] 313 | grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] 314 | grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] 315 | grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] 316 | 317 | [[package]] 318 | name = "google-auth" 319 | version = "2.29.0" 320 | description = "Google Authentication Library" 321 | optional = true 322 | python-versions = ">=3.7" 323 | files = [ 324 | {file = "google-auth-2.29.0.tar.gz", hash = "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360"}, 325 | {file = "google_auth-2.29.0-py2.py3-none-any.whl", hash = "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415"}, 326 | ] 327 | 328 | [package.dependencies] 329 | cachetools = ">=2.0.0,<6.0" 330 | pyasn1-modules = ">=0.2.1" 331 | rsa = ">=3.1.4,<5" 332 | 333 | [package.extras] 334 | aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] 335 | enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] 336 | pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] 337 | reauth = ["pyu2f (>=0.1.5)"] 338 | requests = ["requests (>=2.20.0,<3.0.0.dev0)"] 339 | 340 | [[package]] 341 | name = "google-cloud-bigquery" 342 | version = "3.22.0" 343 | description = "Google BigQuery API client library" 344 | optional = true 345 | python-versions = ">=3.7" 346 | files = [ 347 | {file = "google-cloud-bigquery-3.22.0.tar.gz", hash = "sha256:957591e6f948d7cb4aa0f7a8e4e47b4617cd7f0269e28a71c37953c39b6e8a4c"}, 348 | {file = "google_cloud_bigquery-3.22.0-py2.py3-none-any.whl", hash = "sha256:80c8e31a23b68b7d3ae5d138c9a9edff69d100ee812db73a5e63c79a13a5063d"}, 349 | ] 350 | 351 | [package.dependencies] 352 | google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} 353 | google-auth = ">=2.14.1,<3.0.0dev" 354 | google-cloud-core = ">=1.6.0,<3.0.0dev" 355 | google-resumable-media = ">=0.6.0,<3.0dev" 356 | packaging = ">=20.0.0" 357 | python-dateutil = ">=2.7.2,<3.0dev" 358 | requests = ">=2.21.0,<3.0.0dev" 359 | 360 | [package.extras] 361 | all = ["Shapely (>=1.8.4,<3.0.0dev)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "importlib-metadata (>=1.0.0)", "ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "proto-plus (>=1.15.0,<2.0.0dev)", "protobuf (>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] 362 | bigquery-v2 = ["proto-plus (>=1.15.0,<2.0.0dev)", "protobuf (>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev)"] 363 | bqstorage = ["google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "pyarrow (>=3.0.0)"] 364 | geopandas = ["Shapely (>=1.8.4,<3.0.0dev)", "geopandas (>=0.9.0,<1.0dev)"] 365 | ipython = ["ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)"] 366 | ipywidgets = ["ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)"] 367 | opentelemetry = ["opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)"] 368 | pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "importlib-metadata (>=1.0.0)", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] 369 | tqdm = ["tqdm (>=4.7.4,<5.0.0dev)"] 370 | 371 | [[package]] 372 | name = "google-cloud-core" 373 | version = "2.4.1" 374 | description = "Google Cloud API client core library" 375 | optional = true 376 | python-versions = ">=3.7" 377 | files = [ 378 | {file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"}, 379 | {file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"}, 380 | ] 381 | 382 | [package.dependencies] 383 | google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" 384 | google-auth = ">=1.25.0,<3.0dev" 385 | 386 | [package.extras] 387 | grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] 388 | 389 | [[package]] 390 | name = "google-crc32c" 391 | version = "1.5.0" 392 | description = "A python wrapper of the C library 'Google CRC32C'" 393 | optional = true 394 | python-versions = ">=3.7" 395 | files = [ 396 | {file = "google-crc32c-1.5.0.tar.gz", hash = "sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7"}, 397 | {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13"}, 398 | {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346"}, 399 | {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65"}, 400 | {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b"}, 401 | {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02"}, 402 | {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4"}, 403 | {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e"}, 404 | {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c"}, 405 | {file = "google_crc32c-1.5.0-cp310-cp310-win32.whl", hash = "sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee"}, 406 | {file = "google_crc32c-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289"}, 407 | {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273"}, 408 | {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298"}, 409 | {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57"}, 410 | {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438"}, 411 | {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906"}, 412 | {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183"}, 413 | {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd"}, 414 | {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c"}, 415 | {file = "google_crc32c-1.5.0-cp311-cp311-win32.whl", hash = "sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709"}, 416 | {file = "google_crc32c-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968"}, 417 | {file = "google_crc32c-1.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae"}, 418 | {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556"}, 419 | {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f"}, 420 | {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876"}, 421 | {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc"}, 422 | {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c"}, 423 | {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a"}, 424 | {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e"}, 425 | {file = "google_crc32c-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94"}, 426 | {file = "google_crc32c-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740"}, 427 | {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8"}, 428 | {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a"}, 429 | {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946"}, 430 | {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a"}, 431 | {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d"}, 432 | {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a"}, 433 | {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37"}, 434 | {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894"}, 435 | {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a"}, 436 | {file = "google_crc32c-1.5.0-cp38-cp38-win32.whl", hash = "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4"}, 437 | {file = "google_crc32c-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c"}, 438 | {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7"}, 439 | {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d"}, 440 | {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100"}, 441 | {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9"}, 442 | {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57"}, 443 | {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210"}, 444 | {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd"}, 445 | {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96"}, 446 | {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61"}, 447 | {file = "google_crc32c-1.5.0-cp39-cp39-win32.whl", hash = "sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c"}, 448 | {file = "google_crc32c-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541"}, 449 | {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325"}, 450 | {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd"}, 451 | {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091"}, 452 | {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178"}, 453 | {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2"}, 454 | {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d"}, 455 | {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2"}, 456 | {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5"}, 457 | {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462"}, 458 | {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314"}, 459 | {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728"}, 460 | {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88"}, 461 | {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb"}, 462 | {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31"}, 463 | {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93"}, 464 | ] 465 | 466 | [package.extras] 467 | testing = ["pytest"] 468 | 469 | [[package]] 470 | name = "google-resumable-media" 471 | version = "2.7.0" 472 | description = "Utilities for Google Media Downloads and Resumable Uploads" 473 | optional = true 474 | python-versions = ">= 3.7" 475 | files = [ 476 | {file = "google-resumable-media-2.7.0.tar.gz", hash = "sha256:5f18f5fa9836f4b083162064a1c2c98c17239bfda9ca50ad970ccf905f3e625b"}, 477 | {file = "google_resumable_media-2.7.0-py2.py3-none-any.whl", hash = "sha256:79543cfe433b63fd81c0844b7803aba1bb8950b47bedf7d980c38fa123937e08"}, 478 | ] 479 | 480 | [package.dependencies] 481 | google-crc32c = ">=1.0,<2.0dev" 482 | 483 | [package.extras] 484 | aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] 485 | requests = ["requests (>=2.18.0,<3.0.0dev)"] 486 | 487 | [[package]] 488 | name = "googleapis-common-protos" 489 | version = "1.63.0" 490 | description = "Common protobufs used in Google APIs" 491 | optional = true 492 | python-versions = ">=3.7" 493 | files = [ 494 | {file = "googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e"}, 495 | {file = "googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632"}, 496 | ] 497 | 498 | [package.dependencies] 499 | protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" 500 | 501 | [package.extras] 502 | grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] 503 | 504 | [[package]] 505 | name = "grpcio" 506 | version = "1.63.0" 507 | description = "HTTP/2-based RPC framework" 508 | optional = true 509 | python-versions = ">=3.8" 510 | files = [ 511 | {file = "grpcio-1.63.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2e93aca840c29d4ab5db93f94ed0a0ca899e241f2e8aec6334ab3575dc46125c"}, 512 | {file = "grpcio-1.63.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:91b73d3f1340fefa1e1716c8c1ec9930c676d6b10a3513ab6c26004cb02d8b3f"}, 513 | {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b3afbd9d6827fa6f475a4f91db55e441113f6d3eb9b7ebb8fb806e5bb6d6bd0d"}, 514 | {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f3f6883ce54a7a5f47db43289a0a4c776487912de1a0e2cc83fdaec9685cc9f"}, 515 | {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf8dae9cc0412cb86c8de5a8f3be395c5119a370f3ce2e69c8b7d46bb9872c8d"}, 516 | {file = "grpcio-1.63.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:08e1559fd3b3b4468486b26b0af64a3904a8dbc78d8d936af9c1cf9636eb3e8b"}, 517 | {file = "grpcio-1.63.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5c039ef01516039fa39da8a8a43a95b64e288f79f42a17e6c2904a02a319b357"}, 518 | {file = "grpcio-1.63.0-cp310-cp310-win32.whl", hash = "sha256:ad2ac8903b2eae071055a927ef74121ed52d69468e91d9bcbd028bd0e554be6d"}, 519 | {file = "grpcio-1.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2e44f59316716532a993ca2966636df6fbe7be4ab6f099de6815570ebe4383a"}, 520 | {file = "grpcio-1.63.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:f28f8b2db7b86c77916829d64ab21ff49a9d8289ea1564a2b2a3a8ed9ffcccd3"}, 521 | {file = "grpcio-1.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:65bf975639a1f93bee63ca60d2e4951f1b543f498d581869922910a476ead2f5"}, 522 | {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:b5194775fec7dc3dbd6a935102bb156cd2c35efe1685b0a46c67b927c74f0cfb"}, 523 | {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4cbb2100ee46d024c45920d16e888ee5d3cf47c66e316210bc236d5bebc42b3"}, 524 | {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff737cf29b5b801619f10e59b581869e32f400159e8b12d7a97e7e3bdeee6a2"}, 525 | {file = "grpcio-1.63.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd1e68776262dd44dedd7381b1a0ad09d9930ffb405f737d64f505eb7f77d6c7"}, 526 | {file = "grpcio-1.63.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93f45f27f516548e23e4ec3fbab21b060416007dbe768a111fc4611464cc773f"}, 527 | {file = "grpcio-1.63.0-cp311-cp311-win32.whl", hash = "sha256:878b1d88d0137df60e6b09b74cdb73db123f9579232c8456f53e9abc4f62eb3c"}, 528 | {file = "grpcio-1.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:756fed02dacd24e8f488f295a913f250b56b98fb793f41d5b2de6c44fb762434"}, 529 | {file = "grpcio-1.63.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:93a46794cc96c3a674cdfb59ef9ce84d46185fe9421baf2268ccb556f8f81f57"}, 530 | {file = "grpcio-1.63.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a7b19dfc74d0be7032ca1eda0ed545e582ee46cd65c162f9e9fc6b26ef827dc6"}, 531 | {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:8064d986d3a64ba21e498b9a376cbc5d6ab2e8ab0e288d39f266f0fca169b90d"}, 532 | {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:219bb1848cd2c90348c79ed0a6b0ea51866bc7e72fa6e205e459fedab5770172"}, 533 | {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2d60cd1d58817bc5985fae6168d8b5655c4981d448d0f5b6194bbcc038090d2"}, 534 | {file = "grpcio-1.63.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e350cb096e5c67832e9b6e018cf8a0d2a53b2a958f6251615173165269a91b0"}, 535 | {file = "grpcio-1.63.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:56cdf96ff82e3cc90dbe8bac260352993f23e8e256e063c327b6cf9c88daf7a9"}, 536 | {file = "grpcio-1.63.0-cp312-cp312-win32.whl", hash = "sha256:3a6d1f9ea965e750db7b4ee6f9fdef5fdf135abe8a249e75d84b0a3e0c668a1b"}, 537 | {file = "grpcio-1.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:d2497769895bb03efe3187fb1888fc20e98a5f18b3d14b606167dacda5789434"}, 538 | {file = "grpcio-1.63.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:fdf348ae69c6ff484402cfdb14e18c1b0054ac2420079d575c53a60b9b2853ae"}, 539 | {file = "grpcio-1.63.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a3abfe0b0f6798dedd2e9e92e881d9acd0fdb62ae27dcbbfa7654a57e24060c0"}, 540 | {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:6ef0ad92873672a2a3767cb827b64741c363ebaa27e7f21659e4e31f4d750280"}, 541 | {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b416252ac5588d9dfb8a30a191451adbf534e9ce5f56bb02cd193f12d8845b7f"}, 542 | {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b77eaefc74d7eb861d3ffbdf91b50a1bb1639514ebe764c47773b833fa2d91"}, 543 | {file = "grpcio-1.63.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b005292369d9c1f80bf70c1db1c17c6c342da7576f1c689e8eee4fb0c256af85"}, 544 | {file = "grpcio-1.63.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cdcda1156dcc41e042d1e899ba1f5c2e9f3cd7625b3d6ebfa619806a4c1aadda"}, 545 | {file = "grpcio-1.63.0-cp38-cp38-win32.whl", hash = "sha256:01799e8649f9e94ba7db1aeb3452188048b0019dc37696b0f5ce212c87c560c3"}, 546 | {file = "grpcio-1.63.0-cp38-cp38-win_amd64.whl", hash = "sha256:6a1a3642d76f887aa4009d92f71eb37809abceb3b7b5a1eec9c554a246f20e3a"}, 547 | {file = "grpcio-1.63.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:75f701ff645858a2b16bc8c9fc68af215a8bb2d5a9b647448129de6e85d52bce"}, 548 | {file = "grpcio-1.63.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cacdef0348a08e475a721967f48206a2254a1b26ee7637638d9e081761a5ba86"}, 549 | {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:0697563d1d84d6985e40ec5ec596ff41b52abb3fd91ec240e8cb44a63b895094"}, 550 | {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6426e1fb92d006e47476d42b8f240c1d916a6d4423c5258ccc5b105e43438f61"}, 551 | {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48cee31bc5f5a31fb2f3b573764bd563aaa5472342860edcc7039525b53e46a"}, 552 | {file = "grpcio-1.63.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:50344663068041b34a992c19c600236e7abb42d6ec32567916b87b4c8b8833b3"}, 553 | {file = "grpcio-1.63.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:259e11932230d70ef24a21b9fb5bb947eb4703f57865a404054400ee92f42f5d"}, 554 | {file = "grpcio-1.63.0-cp39-cp39-win32.whl", hash = "sha256:a44624aad77bf8ca198c55af811fd28f2b3eaf0a50ec5b57b06c034416ef2d0a"}, 555 | {file = "grpcio-1.63.0-cp39-cp39-win_amd64.whl", hash = "sha256:166e5c460e5d7d4656ff9e63b13e1f6029b122104c1633d5f37eaea348d7356d"}, 556 | {file = "grpcio-1.63.0.tar.gz", hash = "sha256:f3023e14805c61bc439fb40ca545ac3d5740ce66120a678a3c6c2c55b70343d1"}, 557 | ] 558 | 559 | [package.extras] 560 | protobuf = ["grpcio-tools (>=1.63.0)"] 561 | 562 | [[package]] 563 | name = "grpcio-status" 564 | version = "1.62.2" 565 | description = "Status proto mapping for gRPC" 566 | optional = true 567 | python-versions = ">=3.6" 568 | files = [ 569 | {file = "grpcio-status-1.62.2.tar.gz", hash = "sha256:62e1bfcb02025a1cd73732a2d33672d3e9d0df4d21c12c51e0bbcaf09bab742a"}, 570 | {file = "grpcio_status-1.62.2-py3-none-any.whl", hash = "sha256:206ddf0eb36bc99b033f03b2c8e95d319f0044defae9b41ae21408e7e0cda48f"}, 571 | ] 572 | 573 | [package.dependencies] 574 | googleapis-common-protos = ">=1.5.5" 575 | grpcio = ">=1.62.2" 576 | protobuf = ">=4.21.6" 577 | 578 | [[package]] 579 | name = "idna" 580 | version = "3.7" 581 | description = "Internationalized Domain Names in Applications (IDNA)" 582 | optional = false 583 | python-versions = ">=3.5" 584 | files = [ 585 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 586 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 587 | ] 588 | 589 | [[package]] 590 | name = "packaging" 591 | version = "24.0" 592 | description = "Core utilities for Python packages" 593 | optional = true 594 | python-versions = ">=3.7" 595 | files = [ 596 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 597 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 598 | ] 599 | 600 | [[package]] 601 | name = "platformdirs" 602 | version = "4.2.2" 603 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 604 | optional = true 605 | python-versions = ">=3.8" 606 | files = [ 607 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 608 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 609 | ] 610 | 611 | [package.extras] 612 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 613 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 614 | type = ["mypy (>=1.8)"] 615 | 616 | [[package]] 617 | name = "proto-plus" 618 | version = "1.23.0" 619 | description = "Beautiful, Pythonic protocol buffers." 620 | optional = true 621 | python-versions = ">=3.6" 622 | files = [ 623 | {file = "proto-plus-1.23.0.tar.gz", hash = "sha256:89075171ef11988b3fa157f5dbd8b9cf09d65fffee97e29ce403cd8defba19d2"}, 624 | {file = "proto_plus-1.23.0-py3-none-any.whl", hash = "sha256:a829c79e619e1cf632de091013a4173deed13a55f326ef84f05af6f50ff4c82c"}, 625 | ] 626 | 627 | [package.dependencies] 628 | protobuf = ">=3.19.0,<5.0.0dev" 629 | 630 | [package.extras] 631 | testing = ["google-api-core[grpc] (>=1.31.5)"] 632 | 633 | [[package]] 634 | name = "protobuf" 635 | version = "4.25.3" 636 | description = "" 637 | optional = true 638 | python-versions = ">=3.8" 639 | files = [ 640 | {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, 641 | {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, 642 | {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, 643 | {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, 644 | {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, 645 | {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, 646 | {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, 647 | {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, 648 | {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, 649 | {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, 650 | {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, 651 | ] 652 | 653 | [[package]] 654 | name = "psycopg2" 655 | version = "2.9.9" 656 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 657 | optional = true 658 | python-versions = ">=3.7" 659 | files = [ 660 | {file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, 661 | {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, 662 | {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, 663 | {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, 664 | {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, 665 | {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, 666 | {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, 667 | {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, 668 | {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, 669 | {file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"}, 670 | {file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"}, 671 | {file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"}, 672 | {file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, 673 | ] 674 | 675 | [[package]] 676 | name = "pyasn1" 677 | version = "0.6.0" 678 | description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" 679 | optional = true 680 | python-versions = ">=3.8" 681 | files = [ 682 | {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, 683 | {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, 684 | ] 685 | 686 | [[package]] 687 | name = "pyasn1-modules" 688 | version = "0.4.0" 689 | description = "A collection of ASN.1-based protocols modules" 690 | optional = true 691 | python-versions = ">=3.8" 692 | files = [ 693 | {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, 694 | {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, 695 | ] 696 | 697 | [package.dependencies] 698 | pyasn1 = ">=0.4.6,<0.7.0" 699 | 700 | [[package]] 701 | name = "pycparser" 702 | version = "2.22" 703 | description = "C parser in Python" 704 | optional = false 705 | python-versions = ">=3.8" 706 | files = [ 707 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 708 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 709 | ] 710 | 711 | [[package]] 712 | name = "pygithub" 713 | version = "2.3.0" 714 | description = "Use the full Github API v3" 715 | optional = false 716 | python-versions = ">=3.7" 717 | files = [ 718 | {file = "PyGithub-2.3.0-py3-none-any.whl", hash = "sha256:65b499728be3ce7b0cd2cd760da3b32f0f4d7bc55e5e0677617f90f6564e793e"}, 719 | {file = "PyGithub-2.3.0.tar.gz", hash = "sha256:0148d7347a1cdeed99af905077010aef81a4dad988b0ba51d4108bf66b443f7e"}, 720 | ] 721 | 722 | [package.dependencies] 723 | Deprecated = "*" 724 | pyjwt = {version = ">=2.4.0", extras = ["crypto"]} 725 | pynacl = ">=1.4.0" 726 | requests = ">=2.14.0" 727 | typing-extensions = ">=4.0.0" 728 | urllib3 = ">=1.26.0" 729 | 730 | [[package]] 731 | name = "pyjwt" 732 | version = "2.8.0" 733 | description = "JSON Web Token implementation in Python" 734 | optional = false 735 | python-versions = ">=3.7" 736 | files = [ 737 | {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, 738 | {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, 739 | ] 740 | 741 | [package.dependencies] 742 | cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} 743 | 744 | [package.extras] 745 | crypto = ["cryptography (>=3.4.0)"] 746 | dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] 747 | docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] 748 | tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] 749 | 750 | [[package]] 751 | name = "pynacl" 752 | version = "1.5.0" 753 | description = "Python binding to the Networking and Cryptography (NaCl) library" 754 | optional = false 755 | python-versions = ">=3.6" 756 | files = [ 757 | {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, 758 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, 759 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, 760 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, 761 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, 762 | {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, 763 | {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, 764 | {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, 765 | {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, 766 | {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, 767 | ] 768 | 769 | [package.dependencies] 770 | cffi = ">=1.4.1" 771 | 772 | [package.extras] 773 | docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] 774 | tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] 775 | 776 | [[package]] 777 | name = "pyopenssl" 778 | version = "24.1.0" 779 | description = "Python wrapper module around the OpenSSL library" 780 | optional = true 781 | python-versions = ">=3.7" 782 | files = [ 783 | {file = "pyOpenSSL-24.1.0-py3-none-any.whl", hash = "sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad"}, 784 | {file = "pyOpenSSL-24.1.0.tar.gz", hash = "sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f"}, 785 | ] 786 | 787 | [package.dependencies] 788 | cryptography = ">=41.0.5,<43" 789 | 790 | [package.extras] 791 | docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] 792 | test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] 793 | 794 | [[package]] 795 | name = "python-dateutil" 796 | version = "2.9.0.post0" 797 | description = "Extensions to the standard Python datetime module" 798 | optional = true 799 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 800 | files = [ 801 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 802 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 803 | ] 804 | 805 | [package.dependencies] 806 | six = ">=1.5" 807 | 808 | [[package]] 809 | name = "pytz" 810 | version = "2024.1" 811 | description = "World timezone definitions, modern and historical" 812 | optional = true 813 | python-versions = "*" 814 | files = [ 815 | {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, 816 | {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, 817 | ] 818 | 819 | [[package]] 820 | name = "requests" 821 | version = "2.31.0" 822 | description = "Python HTTP for Humans." 823 | optional = false 824 | python-versions = ">=3.7" 825 | files = [ 826 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 827 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 828 | ] 829 | 830 | [package.dependencies] 831 | certifi = ">=2017.4.17" 832 | charset-normalizer = ">=2,<4" 833 | idna = ">=2.5,<4" 834 | urllib3 = ">=1.21.1,<3" 835 | 836 | [package.extras] 837 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 838 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 839 | 840 | [[package]] 841 | name = "rsa" 842 | version = "4.9" 843 | description = "Pure-Python RSA implementation" 844 | optional = true 845 | python-versions = ">=3.6,<4" 846 | files = [ 847 | {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, 848 | {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, 849 | ] 850 | 851 | [package.dependencies] 852 | pyasn1 = ">=0.1.3" 853 | 854 | [[package]] 855 | name = "six" 856 | version = "1.16.0" 857 | description = "Python 2 and 3 compatibility utilities" 858 | optional = true 859 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 860 | files = [ 861 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 862 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 863 | ] 864 | 865 | [[package]] 866 | name = "snowflake-connector-python" 867 | version = "3.10.0" 868 | description = "Snowflake Connector for Python" 869 | optional = true 870 | python-versions = ">=3.8" 871 | files = [ 872 | {file = "snowflake_connector_python-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e2afca4bca70016519d1a7317c498f1d9c56140bf3e40ea40bddcc95fe827ca"}, 873 | {file = "snowflake_connector_python-3.10.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:d19bde29f89b226eb22af4c83134ecb5c229da1d5e960a01b8f495df78dcdc36"}, 874 | {file = "snowflake_connector_python-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfe013ed97b4dd2e191fd6770a14030d29dd0108817d6ce76b9773250dd2d560"}, 875 | {file = "snowflake_connector_python-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0917c9f9382d830907e1a18ee1208537b203618700a9c671c2a20167b30f574"}, 876 | {file = "snowflake_connector_python-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e828bc99240433e6552ac4cc4e37f223ae5c51c7880458ddb281668503c7491"}, 877 | {file = "snowflake_connector_python-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a0d3d06d758455c50b998eabc1fd972a1f67faa5c85ef250fd5986f5a41aab0b"}, 878 | {file = "snowflake_connector_python-3.10.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4602cb19b204bb03e03d65c6d5328467c9efc0fec53ca56768c3747c8dc8a70f"}, 879 | {file = "snowflake_connector_python-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb1a04b496bbd3e1e2e926df82b2369887b2eea958f535fb934c240bfbabf6c5"}, 880 | {file = "snowflake_connector_python-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c889f9f60f915d657e0a0ad2e6cc52cdcafd9bcbfa95a095aadfd8bcae62b819"}, 881 | {file = "snowflake_connector_python-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e441484216ed416a6ed338133e23bd991ac4ba2e46531f4d330f61568c49314"}, 882 | {file = "snowflake_connector_python-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bb4aced19053c67513cecc92311fa9d3b507b2277698c8e987d404f6f3a49fb2"}, 883 | {file = "snowflake_connector_python-3.10.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:858315a2feff86213b079c6293ad8d850a778044c664686802ead8bb1337e1bc"}, 884 | {file = "snowflake_connector_python-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adf16e1ca9f46d3bdf68e955ffa42075ebdb251e3b13b59003d04e4fea7d579a"}, 885 | {file = "snowflake_connector_python-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4c5c2a08b39086a5348502652ad4fdf24871d7ab30fd59f6b7b57249158468c"}, 886 | {file = "snowflake_connector_python-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:05011286f42c52eb3e5a6db59ee3eaf79f3039f3a19d7ffac6f4ee143779c637"}, 887 | {file = "snowflake_connector_python-3.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:569301289ada5b0d72d0bd8432b7ca180220335faa6d9a0f7185f60891db6f2c"}, 888 | {file = "snowflake_connector_python-3.10.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4e5641c70a12da9804b74f350b8cbbdffdc7aca5069b096755abd2a1fdcf5d1b"}, 889 | {file = "snowflake_connector_python-3.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12ff767a1b8c48431549ac28884f8bd9647e63a23f470b05f6ab8d143c4b1475"}, 890 | {file = "snowflake_connector_python-3.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52bbc1e2e7bda956525b4229d7f87579f8cabd7d5506b12aa754c4bcdc8c8d7"}, 891 | {file = "snowflake_connector_python-3.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:280a8dcca0249e864419564e38764c08f8841900d9872fec2f2855fda494b29f"}, 892 | {file = "snowflake_connector_python-3.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:67bf570230b0cf818e6766c17245c7355a1f5ea27778e54ab8d09e5bb3536ad9"}, 893 | {file = "snowflake_connector_python-3.10.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:aa1e26f9c571d2c4206da5c978c1b345ffd798d3db1f9ae91985e6243c6bf94b"}, 894 | {file = "snowflake_connector_python-3.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73e9baa531d5156a03bfe5af462cf6193ec2a01cbb575edf7a2dd3b2a35254c7"}, 895 | {file = "snowflake_connector_python-3.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e03361c4749e4d65bf0d223fdea1c2d7a33af53b74e873929a6085d150aff17e"}, 896 | {file = "snowflake_connector_python-3.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:e8cddd4357e70ab55d7aeeed144cbbeb1ff658b563d7d8d307afc06178a367ec"}, 897 | {file = "snowflake_connector_python-3.10.0.tar.gz", hash = "sha256:7c7438e958753bd1174b73581d77c92b0b47a86c38d8ea0ba1ea23c442eb8e75"}, 898 | ] 899 | 900 | [package.dependencies] 901 | asn1crypto = ">0.24.0,<2.0.0" 902 | certifi = ">=2017.4.17" 903 | cffi = ">=1.9,<2.0.0" 904 | charset-normalizer = ">=2,<4" 905 | cryptography = ">=3.1.0,<43.0.0" 906 | filelock = ">=3.5,<4" 907 | idna = ">=2.5,<4" 908 | packaging = "*" 909 | platformdirs = ">=2.6.0,<5.0.0" 910 | pyjwt = "<3.0.0" 911 | pyOpenSSL = ">=16.2.0,<25.0.0" 912 | pytz = "*" 913 | requests = "<3.0.0" 914 | sortedcontainers = ">=2.4.0" 915 | tomlkit = "*" 916 | typing-extensions = ">=4.3,<5" 917 | urllib3 = {version = ">=1.21.1,<2.0.0", markers = "python_version < \"3.10\""} 918 | 919 | [package.extras] 920 | development = ["Cython", "coverage", "more-itertools", "numpy (<1.27.0)", "pendulum (!=2.1.1)", "pexpect", "pytest (<7.5.0)", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist", "pytzdata"] 921 | pandas = ["pandas (>=1.0.0,<3.0.0)", "pyarrow"] 922 | secure-local-storage = ["keyring (>=23.1.0,<25.0.0)"] 923 | 924 | [[package]] 925 | name = "sortedcontainers" 926 | version = "2.4.0" 927 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 928 | optional = true 929 | python-versions = "*" 930 | files = [ 931 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 932 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 933 | ] 934 | 935 | [[package]] 936 | name = "tomlkit" 937 | version = "0.12.5" 938 | description = "Style preserving TOML library" 939 | optional = true 940 | python-versions = ">=3.7" 941 | files = [ 942 | {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, 943 | {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, 944 | ] 945 | 946 | [[package]] 947 | name = "typing-extensions" 948 | version = "4.11.0" 949 | description = "Backported and Experimental Type Hints for Python 3.8+" 950 | optional = false 951 | python-versions = ">=3.8" 952 | files = [ 953 | {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, 954 | {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, 955 | ] 956 | 957 | [[package]] 958 | name = "urllib3" 959 | version = "1.26.18" 960 | description = "HTTP library with thread-safe connection pooling, file post, and more." 961 | optional = false 962 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 963 | files = [ 964 | {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, 965 | {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, 966 | ] 967 | 968 | [package.extras] 969 | brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 970 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 971 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 972 | 973 | [[package]] 974 | name = "wrapt" 975 | version = "1.16.0" 976 | description = "Module for decorators, wrappers and monkey patching." 977 | optional = false 978 | python-versions = ">=3.6" 979 | files = [ 980 | {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, 981 | {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, 982 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, 983 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, 984 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, 985 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, 986 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, 987 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, 988 | {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, 989 | {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, 990 | {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, 991 | {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, 992 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, 993 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, 994 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, 995 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, 996 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, 997 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, 998 | {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, 999 | {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, 1000 | {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, 1001 | {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, 1002 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, 1003 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, 1004 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, 1005 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, 1006 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, 1007 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, 1008 | {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, 1009 | {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, 1010 | {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, 1011 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, 1012 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, 1013 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, 1014 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, 1015 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, 1016 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, 1017 | {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, 1018 | {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, 1019 | {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, 1020 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, 1021 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, 1022 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, 1023 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, 1024 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, 1025 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, 1026 | {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, 1027 | {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, 1028 | {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, 1029 | {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, 1030 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, 1031 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, 1032 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, 1033 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, 1034 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, 1035 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, 1036 | {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, 1037 | {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, 1038 | {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, 1039 | {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, 1040 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, 1041 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, 1042 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, 1043 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, 1044 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, 1045 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, 1046 | {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, 1047 | {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, 1048 | {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, 1049 | {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, 1050 | ] 1051 | 1052 | [extras] 1053 | bigquery = ["google-cloud-bigquery"] 1054 | postgres = ["psycopg2"] 1055 | redshift = ["psycopg2"] 1056 | snowflake = ["snowflake-connector-python"] 1057 | 1058 | [metadata] 1059 | lock-version = "2.0" 1060 | python-versions = "^3.9" 1061 | content-hash = "f0ee5de614a387f23119ee224425aeb92147fe9a402807bd9ac185f18a6449d4" 1062 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyproject.toml 2 | 3 | [tool.poetry] 4 | name = "optician" 5 | version = "1.1.1" 6 | description = "Sync your data warehouse tables to Looker" 7 | authors = [ 8 | "GetGround " 9 | ] 10 | readme = "README.md" 11 | license = "MIT" 12 | repository = "https://github.com/getground/optician" 13 | keywords = ["optician", "looker", "sync", "lookml", "dbt", "data warehouse"] 14 | classifiers = [ 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python", 17 | "Programming Language :: Python :: 3", 18 | "Topic :: Database", 19 | "Topic :: Software Development :: Libraries :: Python Modules", 20 | "Development Status :: 4 - Beta" 21 | ] 22 | 23 | [project.urls] 24 | Homepage = "https://github.com/getground/optician" 25 | 26 | [tool.poetry.dependencies] 27 | # These packages are mandatory 28 | python = "^3.9" 29 | PyGithub = "^2.3.0" 30 | # The packages are optional dependencies 31 | google-cloud-bigquery = {version = "^3.10.0", optional = true } 32 | psycopg2 = {version = "^2.9.9", optional = true} 33 | snowflake-connector-python = {version = "^3.8.1", optional = true} 34 | 35 | [tool.poetry.extras] 36 | bigquery = ["google-cloud-bigquery"] 37 | redshift = ["psycopg2"] 38 | postgres = ["psycopg2"] 39 | snowflake = ["snowflake-connector-python"] 40 | 41 | [build-system] 42 | requires = ["poetry-core"] 43 | build-backend = "poetry.core.masonry.api" 44 | 45 | [tool.poetry.scripts] 46 | optician = "optician.cli.commands:cli" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/optician/__main__.py: -------------------------------------------------------------------------------- 1 | from optician.cli.commands import cli 2 | 3 | if __name__ == "__main__": 4 | cli() 5 | -------------------------------------------------------------------------------- /src/optician/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getground/optician/f18e9330bb6d33c07003d65f9577e8b28ae6f77a/src/optician/cli/__init__.py -------------------------------------------------------------------------------- /src/optician/cli/commands.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | from optician.vc_client import GithubClient 6 | from optician.db_client import DbClient as db 7 | from optician.diff_tracker import DiffTracker 8 | from optician.lookml_generator import LookMLGenerator 9 | from optician.logger import Logger 10 | 11 | CONSOLE_LOGGER = Logger().get_logger() 12 | 13 | 14 | def cli(): 15 | parser = argparse.ArgumentParser(description="Command line interface for optician") 16 | 17 | subparsers = parser.add_subparsers(dest="command", title="commands", metavar="") 18 | 19 | # track_diff_models parser 20 | diff_tracker_parser = subparsers.add_parser("diff_tracker", help="Run diff tracker") 21 | diff_tracker_parser.add_argument( 22 | "--db_type", 23 | type=str, 24 | help="Database type (bigquery, redshift, snowflake)", 25 | required=True, 26 | ) 27 | diff_tracker_parser.add_argument( 28 | "--dataset1_name", type=str, help="Dataset 1", required=True 29 | ) 30 | diff_tracker_parser.add_argument( 31 | "--dataset2_name", type=str, help="Dataset 2", required=True 32 | ) 33 | diff_tracker_parser.add_argument( 34 | "--project", type=str, help="Project ID", required=True 35 | ) 36 | # This argument is optional, but if it is not provided it will default to False 37 | diff_tracker_parser.add_argument( 38 | "--full-refresh", 39 | help="Full refresh of LookML base views", 40 | action=argparse.BooleanOptionalAction, 41 | ) 42 | diff_tracker_parser.add_argument( 43 | "--service_account", type=str, help="Google Service Account", required=False 44 | ) 45 | diff_tracker_parser.add_argument( 46 | "--models", 47 | type=str, 48 | help="List of models to compare (comma separated) or file path", 49 | ) 50 | diff_tracker_parser.add_argument("--output", type=str, help="Output file path") 51 | 52 | # generate_lookml parser 53 | generate_lookml_parser = subparsers.add_parser( 54 | "generate_lookml", help="Run generate LookML" 55 | ) 56 | generate_lookml_parser.add_argument( 57 | "--db_type", 58 | type=str, 59 | help="Database type (bigquery, redshift, snowflake)", 60 | required=True, 61 | ) 62 | generate_lookml_parser.add_argument( 63 | "--project", type=str, help="Project ID", required=True 64 | ) 65 | generate_lookml_parser.add_argument( 66 | "--dataset", type=str, help="Dataset ID to read the models from", required=True 67 | ) 68 | generate_lookml_parser.add_argument( 69 | "--tables", 70 | type=str, 71 | help="List of Table IDs separated by comma or provide a file path", 72 | required=True, 73 | ) 74 | generate_lookml_parser.add_argument( 75 | "--output-dir", type=str, help="Output Directory", required=False 76 | ) 77 | generate_lookml_parser.add_argument( 78 | "--override-dataset-id", type=str, help="Override Dataset ID", required=False 79 | ) 80 | generate_lookml_parser.add_argument( 81 | "--service-account", type=str, help="Service Account", required=False 82 | ) 83 | 84 | # push_to_looker 85 | push_to_looker_parser = subparsers.add_parser( 86 | "push_to_looker", help="Run Push to Looker" 87 | ) 88 | push_to_looker_parser.add_argument( 89 | "--token", type=str, help="GitHub Token", required=True 90 | ) 91 | push_to_looker_parser.add_argument( 92 | "--repo", type=str, help="GitHub Repo", required=True 93 | ) 94 | push_to_looker_parser.add_argument( 95 | "--user-email", type=str, help="GitHub User Email", required=False 96 | ) 97 | push_to_looker_parser.add_argument( 98 | "--input-dir", 99 | type=str, 100 | help="Path that contains new files to be committed", 101 | required=True, 102 | ) 103 | push_to_looker_parser.add_argument( 104 | "--output-dir", 105 | type=str, 106 | help="Directory to write the LookML files to", 107 | required=True, 108 | ) 109 | push_to_looker_parser.add_argument( 110 | "--branch-name", 111 | type=str, 112 | help="Name of the branch to be created", 113 | required=True, 114 | ) 115 | push_to_looker_parser.add_argument( 116 | "--base-branch", 117 | type=str, 118 | help="Name of the base branch", 119 | default="main", 120 | ) 121 | 122 | args = parser.parse_args() 123 | if args.command == "diff_tracker": 124 | models = args.models.split(",") 125 | if len(models) == 1: 126 | # If the tables argument is a file path, read the file and split on newlines 127 | if "." in models[0]: 128 | models_file_path = models[0] 129 | with open(models_file_path, "r") as file: 130 | models = file.read().splitlines() 131 | 132 | CONSOLE_LOGGER.info(f"Models to be compared: {models}") 133 | 134 | credentials = { 135 | "service_account": args.service_account, 136 | "project_id": args.project, 137 | # Add other credentials for other databases here 138 | } 139 | 140 | db_client = db(db_type=args.db_type, credentials=credentials) 141 | 142 | dt = DiffTracker( 143 | dataset1_name=args.dataset1_name, 144 | dataset2_name=args.dataset2_name, 145 | db_client=db_client, 146 | models=models, 147 | full_refresh=args.full_refresh, 148 | ) 149 | results = dt.get_diff_tables() 150 | CONSOLE_LOGGER.info(f"New models: {results['new_models']}") 151 | CONSOLE_LOGGER.info(f"Diff models: {results['diff_models']}") 152 | CONSOLE_LOGGER.info(f"Missing models: {results['missing_models']}") 153 | 154 | # Save output to file 155 | output = results["diff_models"] 156 | output += results["new_models"] 157 | with open(args.output, "w") as f: 158 | for o in output: 159 | f.write(f"{o}\n") 160 | pass 161 | 162 | elif args.command == "generate_lookml": 163 | tables = args.tables.split(",") 164 | if len(tables) == 1: 165 | # If the tables argument is a file path, read the file and split on newlines 166 | if "." in tables[0]: 167 | tables_file_path = tables[0] 168 | with open(tables_file_path, "r") as file: 169 | tables = file.read().splitlines() 170 | 171 | CONSOLE_LOGGER.info(f"Models to be created: {tables}") 172 | 173 | credentials = { 174 | "service_account": args.service_account, 175 | "project_id": args.project 176 | # Add other credentials for other databases here 177 | } 178 | 179 | db_client = db(db_type=args.db_type, credentials=credentials) 180 | lookml = LookMLGenerator(db_client, args.dataset) 181 | lookml.generate_batch_lookml_views( 182 | tables=tables, 183 | output_dir=args.output_dir, 184 | override_dataset_id=args.override_dataset_id, 185 | ) 186 | 187 | elif args.command == "push_to_looker": 188 | if not os.path.exists(args.input_dir): 189 | CONSOLE_LOGGER.warn( 190 | f"Input directory {args.input_dir} does not exist. No files to commit. Exiting..." 191 | ) 192 | 193 | else: 194 | # Create Github client 195 | 196 | G = GithubClient( 197 | token=args.token, 198 | repo=args.repo, 199 | user_email=args.user_email, 200 | ) 201 | 202 | # Create branch and commit files 203 | G.update_files( 204 | input_dir=args.input_dir, 205 | output_dir=args.output_dir, 206 | target_branch=args.branch_name, 207 | base_branch=args.base_branch, 208 | ) 209 | 210 | 211 | def execute_from_command_line(): 212 | cli() 213 | -------------------------------------------------------------------------------- /src/optician/db_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .db_client import * 2 | -------------------------------------------------------------------------------- /src/optician/db_client/db_client.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | 4 | class DbClient: 5 | SUPPORTED_DATABASES = ["bigquery"] 6 | 7 | def __init__(self, db_type: str, credentials: dict): 8 | self.db_type = db_type 9 | self.credentials = credentials 10 | self.db_client = None 11 | 12 | if self.db_type == "bigquery": 13 | self.db_client = BQClient( 14 | project_id=self.credentials.get("project_id", None), 15 | service_account=self.credentials.get("service_account", None), 16 | ) 17 | else: 18 | raise Exception(f"Database type {self.db_type} not supported") 19 | 20 | def get_table(self, *args, **kwargs): 21 | return self.db_client.get_table(*args, **kwargs) 22 | 23 | def list_tables(self, *args, **kwargs): 24 | return self.db_client.list_tables(*args, **kwargs) 25 | 26 | # Defining the method in the client class since the definition 27 | # may differ between databases 28 | def is_nested_field(self, field): 29 | return self.db_client.is_nested_field(field) 30 | 31 | 32 | class BQClient: 33 | def __init__(self, project_id: str, service_account: str = None): 34 | bigquery = import_module("google.cloud.bigquery") 35 | 36 | self.project_id = project_id 37 | if not self.project_id: 38 | raise ValueError("Project ID is required for BigQuery client") 39 | self.service_account = service_account 40 | 41 | if self.service_account: 42 | # Create BigQuery client with service account 43 | self.bq = bigquery.Client.from_service_account_json( 44 | self.service_account, project=self.project_id 45 | ) 46 | else: 47 | # Create BigQuery client with oauth 48 | self.bq = bigquery.Client(project=self.project_id) 49 | 50 | @staticmethod 51 | def is_nested_field(field): 52 | return field.internal_type == "RECORD" and field.mode == "NULLABLE" 53 | 54 | def get_client(self): 55 | return self.bq 56 | 57 | def get_table(self, dataset_id: str, table_id: str): 58 | # Get BigQuery table from API 59 | bq_table_ref = self.bq.dataset(dataset_id).table(table_id) 60 | bq_table = self.bq.get_table(bq_table_ref) 61 | 62 | # Create Table instance 63 | table = Table(name=bq_table.table_id, internal_schema=bq_table.schema) 64 | 65 | # Create Field instances for each field in the table 66 | for schema_field in table.internal_schema: 67 | field = Field( 68 | name=schema_field.name, 69 | internal_type=schema_field.field_type, 70 | mode=schema_field.mode, 71 | description=schema_field.description, 72 | ) 73 | 74 | # If the field is a nested field, add the nested fields to the schema 75 | if self.is_nested_field(field): 76 | for nested_field in schema_field.fields: 77 | nested_field = Field( 78 | name=nested_field.name, 79 | internal_type=nested_field.field_type, 80 | mode=nested_field.mode, 81 | description=nested_field.description, 82 | ) 83 | field.add_nested_field(nested_field) 84 | 85 | table.add_field_to_schema(field=field) 86 | return table 87 | 88 | def list_tables(self, dataset_id: str): 89 | dataset_ref = self.bq.dataset(dataset_id) 90 | tables = self.bq.list_tables(dataset_ref) 91 | return [self.get_table(dataset_id, table.table_id) for table in tables] 92 | 93 | 94 | class Field: 95 | def __init__( 96 | self, 97 | name: str, 98 | internal_type: str, 99 | mode: str, 100 | description: str, 101 | ) -> None: 102 | """Initialisation of the field 103 | 104 | Args: 105 | name (str): Name of the field 106 | internal_type (str): Type of the field. Depends on the data warehouse engine. 107 | mode (str): Mode of the field. In BigQuery, can be NULLABLE, REQUIRED or REPEATED. 108 | description (str): Description of the field. 109 | """ 110 | self.name = name 111 | self.internal_type = internal_type 112 | self.description = description 113 | self.mode = mode 114 | self.fields = [] 115 | 116 | def __eq__(self, other): 117 | if not isinstance(other, Field): 118 | return False 119 | return ( 120 | self.name == other.name 121 | and self.internal_type == other.internal_type 122 | and self.mode == other.mode 123 | and self.description == other.description 124 | and self.fields == other.fields 125 | ) 126 | 127 | def get_name(self): 128 | return self.name 129 | 130 | def get_internal_type(self): 131 | return self.internal_type 132 | 133 | def get_mode(self): 134 | return self.mode 135 | 136 | def get_description(self): 137 | return self.description 138 | 139 | def add_nested_field(self, field): 140 | self.fields.append(field) 141 | 142 | 143 | class Table: 144 | def __init__(self, name: str, internal_schema) -> None: 145 | """Initialisation of the table. 146 | 147 | Args: 148 | name (str): Name of the table in the dbt dataset. 149 | internal_schema (_type_): Object from the client that contains the table schema. 150 | """ 151 | self.name = name 152 | self.description = None 153 | self.internal_schema = internal_schema 154 | self.schema = [] 155 | 156 | def __eq__(self, other): 157 | if not isinstance(other, Table): 158 | return False 159 | 160 | return ( 161 | self.name == other.name 162 | and self.description == other.description 163 | and self.schema == other.schema 164 | ) 165 | 166 | def add_field_to_schema(self, field: Field): 167 | self.schema.append(field) 168 | 169 | def get_table_name(self): 170 | return self.name 171 | 172 | def get_internal_schema(self): 173 | return self.internal_schema 174 | 175 | def get_schema(self): 176 | return self.schema 177 | 178 | def get_schema_as_dict(self): 179 | return self.schema.__dict__ 180 | -------------------------------------------------------------------------------- /src/optician/diff_tracker/__init__.py: -------------------------------------------------------------------------------- 1 | from .diff_tracker import * 2 | -------------------------------------------------------------------------------- /src/optician/diff_tracker/diff_tracker.py: -------------------------------------------------------------------------------- 1 | from optician.db_client import DbClient 2 | 3 | 4 | class DiffTracker: 5 | def __init__( 6 | self, 7 | dataset1_name: str, 8 | dataset2_name: str, 9 | db_client: DbClient = None, 10 | models: list = None, 11 | full_refresh: bool = False, 12 | ): 13 | self.dataset1_name = dataset1_name 14 | self.dataset2_name = dataset2_name 15 | self.models = models 16 | self.db = db_client 17 | self.full_refresh = full_refresh 18 | 19 | def get_table_schemas(self, dataset_id: str): 20 | # Get list of tables in dataset that are also in models 21 | table_schemas = {} 22 | for table in self.db.list_tables(dataset_id): 23 | table_id = table.name 24 | if table_id in self.models: 25 | table_schemas[table_id] = table 26 | 27 | return table_schemas 28 | 29 | def get_diff_tables(self): 30 | results = {"new_models": [], "diff_models": [], "missing_models": []} 31 | 32 | # Get table schemas for dataset1 33 | dataset1_schemas = self.get_table_schemas(self.dataset1_name) 34 | 35 | # If full refresh, return all tables in dataset1 36 | if self.full_refresh == True: 37 | results["diff_models"] = list(dataset1_schemas.keys()) 38 | return results 39 | 40 | # Get table schemas for dataset2 41 | dataset2_schemas = self.get_table_schemas(self.dataset2_name) 42 | 43 | # Compare schemas 44 | for table_name, schema1 in dataset1_schemas.items(): 45 | if table_name in dataset2_schemas: 46 | schema2 = dataset2_schemas[table_name] 47 | if schema1 != schema2: 48 | results["diff_models"].append(table_name) 49 | else: 50 | results["new_models"].append(table_name) 51 | 52 | for table_name in dataset2_schemas: 53 | if table_name not in dataset1_schemas: 54 | results["missing_models"].append(table_name) 55 | 56 | return results 57 | -------------------------------------------------------------------------------- /src/optician/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import TimedRotatingFileHandler 3 | import os 4 | 5 | 6 | class Logger: 7 | def __init__( 8 | self, 9 | log_file="optician.log", 10 | log_folder="logs", 11 | log_to_console=True, 12 | log_to_file=True, 13 | ): 14 | self.logger = logging.getLogger(__name__) 15 | self.logger.setLevel(logging.DEBUG) 16 | self.log_to_console = log_to_console 17 | self.log_to_file = log_to_file 18 | self.log_file = os.path.join(log_folder, log_file) 19 | 20 | if self.log_to_file: 21 | if not os.path.exists(log_folder): 22 | os.makedirs(log_folder) 23 | 24 | def get_logger(self): 25 | # Check if console handler already exists 26 | if self.log_to_console and not any( 27 | isinstance(handler, logging.StreamHandler) 28 | for handler in self.logger.handlers 29 | ): 30 | console_formatter = logging.Formatter("%(message)s") 31 | console_handler = logging.StreamHandler() 32 | console_handler.setLevel(logging.INFO) 33 | console_handler.setFormatter(console_formatter) 34 | self.logger.addHandler(console_handler) 35 | 36 | # Check if file handler already exists 37 | if self.log_to_file and not any( 38 | isinstance(handler, TimedRotatingFileHandler) 39 | for handler in self.logger.handlers 40 | ): 41 | file_formatter = logging.Formatter( 42 | "[%(asctime)s] - [%(levelname)s] - %(module)s - %(funcName)s - %(message)s" 43 | ) 44 | file_handler = TimedRotatingFileHandler( 45 | filename=self.log_file, when="D", interval=1, backupCount=5 46 | ) 47 | file_handler.setLevel(logging.DEBUG) 48 | file_handler.setFormatter(file_formatter) 49 | self.logger.addHandler(file_handler) 50 | 51 | return self.logger 52 | -------------------------------------------------------------------------------- /src/optician/lookml_generator/__init__.py: -------------------------------------------------------------------------------- 1 | from .lookml_generator import * 2 | -------------------------------------------------------------------------------- /src/optician/lookml_generator/lookml_generator.py: -------------------------------------------------------------------------------- 1 | from optician.db_client import db_client as db 2 | import os 3 | import json 4 | from optician.logger import Logger 5 | 6 | CONSOLE_LOGGER = Logger().get_logger() 7 | 8 | FIELD_TYPE_MAPPING = { 9 | "bigquery": { 10 | "INTEGER": "number", 11 | "INT64": "number", 12 | "FLOAT": "number", 13 | "FLOAT64": "number", 14 | "BIGNUMERIC": "number", 15 | "NUMERIC": "number", 16 | "BOOLEAN": "yesno", 17 | "BOOL": "yesno", 18 | "TIMESTAMP": "time", 19 | "TIME": "time", 20 | "DATE": "time", 21 | "DATETIME": "time", 22 | "STRING": "string", 23 | "ARRAY": "string", 24 | "GEOGRAPHY": "string", 25 | "BYTES": "string", 26 | } 27 | } 28 | 29 | DEFAULT_TIMEFRAMES = [ 30 | "raw", 31 | "time", 32 | "date", 33 | "week", 34 | "month", 35 | "month_name", 36 | "month_num", 37 | "quarter", 38 | "quarter_of_year", 39 | "year", 40 | ] 41 | 42 | CONFIG_OPTIONS = { 43 | "hide_all_fields": {"type": bool}, 44 | "capitalize_ids": {"type": bool}, 45 | "primary_key_columns": {"type": list, "subtype": str}, 46 | "ignore_column_types": {"type": list, "subtype": str}, 47 | "ignore_modes": {"type": list, "subtype": str}, 48 | "timeframes": {"type": list, "subtype": str}, 49 | "time_suffixes": {"type": list, "subtype": str}, 50 | "order_by": {"type": str, "options": ["alpha", "table"]}, 51 | } 52 | 53 | 54 | class Config: 55 | def __init__(self, config_file_path=None): 56 | self._config_file_path = config_file_path 57 | self._custom_config = self._read_custom_config() 58 | self._test_config() 59 | 60 | def _read_custom_config(self): 61 | if self._config_file_path is None: 62 | return {} 63 | else: 64 | try: 65 | return json.load(open(self._config_file_path)) 66 | except: 67 | raise Exception("Invalid config file path") 68 | 69 | def get_property(self, property_name, default_value=None): 70 | if property_name in self._custom_config.keys(): 71 | return self._custom_config.get(property_name) 72 | else: 73 | return default_value 74 | 75 | def _test_config(self): 76 | for property_name in self._custom_config.keys(): 77 | property_value = self.get_property(property_name) 78 | if property_name not in CONFIG_OPTIONS.keys(): 79 | raise Exception(f"Invalid config option: {property_name}") 80 | # check type 81 | if not isinstance(property_value, CONFIG_OPTIONS[property_name]["type"]): 82 | raise Exception( 83 | f"Invalid type for config option {property_name}. Expected {CONFIG_OPTIONS[property_name]['type']}" 84 | ) 85 | # check subtype 86 | if "subtype" in CONFIG_OPTIONS[property_name].keys(): 87 | if len(property_value) > 0: 88 | if not isinstance( 89 | self.get_property(property_name)[0], 90 | CONFIG_OPTIONS[property_name]["subtype"], 91 | ): 92 | raise Exception( 93 | f"Invalid subtype for config option {property_name}. Expected {CONFIG_OPTIONS[property_name]['subtype']}" 94 | ) 95 | if "options" in CONFIG_OPTIONS[property_name].keys(): 96 | if property_value not in CONFIG_OPTIONS[property_name]["options"]: 97 | raise Exception( 98 | f"Invalid value for config option {property_name}. Expected one of {CONFIG_OPTIONS[property_name]['options']}" 99 | ) 100 | 101 | 102 | class LookMLGenerator: 103 | def __init__(self, client: db.DbClient, dataset_id: str): 104 | self.client = client 105 | self.dataset_id = dataset_id 106 | self._config_file_env_name = "OPTICIAN_CONFIG_FILE" 107 | self.config = Config(os.getenv(self._config_file_env_name, None)) 108 | self.hide_all_fields = self.config.get_property("hide_all_fields", False) 109 | self.primary_key_column_names = self.config.get_property( 110 | "primary_key_columns", [] 111 | ) 112 | self.ignore_column_types = self.config.get_property("ignore_column_types", []) 113 | self.ignore_modes = self.config.get_property("ignore_modes", []) 114 | self.timeframes = self.config.get_property("timeframes", DEFAULT_TIMEFRAMES) 115 | self.time_suffixes = self.config.get_property("time_suffixes", []) 116 | self.order_by = self.config.get_property("order_by", "alpha") 117 | self.capitalize_ids = self.config.get_property("capitalize_ids", True) 118 | 119 | def _build_field_name(self, field_name: str): 120 | # remove underscores 121 | field_name = field_name.replace("_", " ") 122 | # title case 123 | field_name = field_name.title() 124 | if self.capitalize_ids: 125 | # capitalize any "id" in the field name 126 | field_name = field_name.replace("Id", "ID") 127 | return field_name 128 | 129 | def _get_looker_type(self, field: db.Field): 130 | # Default unknown types to string 131 | return FIELD_TYPE_MAPPING[self.client.db_type].get( 132 | field.internal_type, "string" 133 | ) 134 | 135 | def _build_timeframes(self): 136 | tf = "timeframes: [\n" 137 | tf += "".join(f" {tf},\n" for tf in self.timeframes[:-1]) 138 | tf += f" {self.timeframes[-1]}\n" 139 | tf += " ]" 140 | return tf 141 | 142 | def process_field(self, field: db.Field, parent_field_name: str = None): 143 | field_name = field.name 144 | field_sql_name = field_name 145 | field_type = field.internal_type 146 | lookml_type = self._get_looker_type(field) 147 | is_nested_field = self.client.is_nested_field(field) 148 | field_mode = field.mode 149 | field_description = field.description 150 | pk = "" 151 | convert_ts = "" 152 | datatype = "" 153 | group_label = "" 154 | group_item_label = "" 155 | hidden = "" 156 | view_field = "" 157 | 158 | if field_type == "DATE": 159 | convert_ts = "convert_tz: no" 160 | datatype = "datatype: date" 161 | 162 | elif field_type == "DATETIME": 163 | datatype = "datatype: datetime" 164 | 165 | field_description = ( 166 | '"' + field_description.rstrip().replace('"', "'") + '"' 167 | if field_description 168 | else '""' 169 | ) 170 | 171 | if parent_field_name: 172 | field_sql_name = parent_field_name + "." + field_name 173 | group_item_label = ( 174 | f'group_item_label: "{self._build_field_name(field_name)}"' 175 | ) 176 | field_name = parent_field_name + "__" + field_name 177 | group_label = f'group_label: "{self._build_field_name(parent_field_name)}"' 178 | 179 | elif field_name in self.primary_key_column_names: 180 | pk = "primary_key: yes" 181 | 182 | if self.hide_all_fields: 183 | hidden = "hidden: yes" 184 | 185 | # Don't write these fields 186 | if field_mode is not None: 187 | if field_mode in self.ignore_modes: 188 | return "" 189 | 190 | if self.ignore_column_types: 191 | if field_type in self.ignore_column_types: 192 | return "" 193 | 194 | # Handle nested fields 195 | if is_nested_field: 196 | # Handle nested fields within a record field 197 | nested_fields = field.fields 198 | # Sort the nested fields by name or leave them in the order they are in 199 | if self.order_by == "alpha": 200 | nested_fields = sorted(nested_fields, key=lambda x: x.name) 201 | nested_output = "" 202 | for nested_field in nested_fields: 203 | # Recursively process the nested field 204 | nested_output += self.process_field( 205 | nested_field, parent_field_name=f"{field_name}" 206 | ) 207 | return nested_output 208 | 209 | # Handle time fields 210 | elif lookml_type == "time": 211 | timeframes = self._build_timeframes() 212 | # if field name ends with _at, _time, or _date 213 | if len(self.time_suffixes) > 0: 214 | for s in self.time_suffixes: 215 | if field_name.endswith(s): 216 | # split field name on underscore and remove last part 217 | field_name = "_".join(field_name.split("_")[:-1]) 218 | break 219 | 220 | view_field = f" dimension_group: {field_name} {{\n" 221 | view_field += f" {hidden}\n" 222 | view_field += f" description: {field_description}\n" 223 | view_field += f" type: time\n" 224 | view_field += f" {timeframes}\n" 225 | view_field += f" {convert_ts}\n" 226 | view_field += f" {datatype}\n" 227 | view_field += f" sql: ${{TABLE}}.{field_sql_name} ;;\n" 228 | view_field += f" }}\n" 229 | 230 | # Handle all other fields 231 | else: 232 | view_field = f" dimension: {field_name} {{\n" 233 | view_field += f" {hidden}\n" 234 | view_field += f" {pk}\n" 235 | view_field += f" {group_label}\n" 236 | view_field += f" {group_item_label}\n" 237 | view_field += f" description: {field_description}\n" 238 | view_field += f" type: {lookml_type}\n" 239 | view_field += f" sql: ${{TABLE}}.{field_sql_name} ;;\n" 240 | view_field += f" }}\n" 241 | 242 | if view_field != "": 243 | # Remove any empty lines from the view field 244 | view_field = "\n".join( 245 | [ll.rstrip() for ll in view_field.splitlines() if ll.strip()] 246 | ) 247 | view_field = "\n" + view_field + "\n" 248 | 249 | return view_field 250 | 251 | def generate_lookml_view( 252 | self, 253 | table_id: str, 254 | output_dir: str = None, 255 | view_name: str = None, 256 | override_dataset_id: str = None, 257 | ): 258 | # Generate LookML view 259 | if not view_name: 260 | view_name = table_id 261 | 262 | if override_dataset_id: 263 | sql_table_name = f"{override_dataset_id}.{table_id}" 264 | else: 265 | sql_table_name = f"{self.dataset_id}.{table_id}" 266 | 267 | view_output = f"view: {view_name} {{\n" 268 | view_output += f" sql_table_name: `{sql_table_name}`;;\n" # Include the SQL table name parameter 269 | 270 | table = self.client.get_table(self.dataset_id, table_id) 271 | 272 | # Sort the fields by name or leave them in the order they are in 273 | if self.order_by == "alpha": 274 | table.schema = sorted(table.schema, key=lambda x: x.name) 275 | 276 | for field in table.schema: 277 | view_field = self.process_field(field) 278 | view_output += view_field 279 | view_output += "\n}" 280 | 281 | # Write the LookML view to a file 282 | lookml_file_path = f"{view_name}.view.lkml" 283 | if output_dir: 284 | # create directory if it doesn't exist 285 | # with all permissions 286 | if not os.path.exists(output_dir): 287 | os.mkdir(output_dir) 288 | 289 | lookml_file_path = os.path.join(output_dir, lookml_file_path) 290 | 291 | with open(lookml_file_path, "w") as file: 292 | file.write(view_output) 293 | 294 | CONSOLE_LOGGER.info(f"LookML view written to {lookml_file_path}") 295 | 296 | def generate_batch_lookml_views( 297 | self, tables: list, output_dir: str = None, override_dataset_id: str = None 298 | ): 299 | for table in tables: 300 | self.generate_lookml_view( 301 | table_id=table, 302 | output_dir=output_dir, 303 | override_dataset_id=override_dataset_id, 304 | ) 305 | -------------------------------------------------------------------------------- /src/optician/vc_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .vc_client import * 2 | -------------------------------------------------------------------------------- /src/optician/vc_client/vc_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | from github import Github, Auth, InputGitAuthor 3 | from github.GithubException import UnknownObjectException 4 | from optician.logger import Logger 5 | 6 | 7 | CONSOLE_LOGGER = Logger().get_logger() 8 | 9 | 10 | class GithubClient: 11 | def __init__(self, token: str, repo: str, user_email: str = None): 12 | self.token = token 13 | self.repo = repo 14 | self.user_email = user_email 15 | # Create a GitHub API client using the access token 16 | auth = Auth.Token(token) 17 | g = Github(auth=auth) 18 | self.repo = g.get_repo(repo) 19 | self.user = g.get_user() 20 | 21 | def update_files( 22 | self, 23 | input_dir: str, 24 | output_dir: str, 25 | target_branch: str, 26 | base_branch: str = "main", 27 | file_creation_message: str = None, 28 | file_update_message: str = None, 29 | ): 30 | # Pass login as email, since it's required but not tested 31 | if not self.user_email: 32 | self.user_email = self.user.login 33 | author = InputGitAuthor(self.user.login, self.user_email) 34 | branches = self.repo.get_branches() 35 | 36 | if target_branch in [b.name for b in branches]: 37 | CONSOLE_LOGGER.info(f"Branch {target_branch} already exists") 38 | branch = self.repo.get_git_ref(f"heads/{target_branch}") 39 | else: 40 | base_ref = self.repo.get_branch(base_branch) 41 | self.repo.create_git_ref( 42 | f"refs/heads/{target_branch}", sha=base_ref.commit.sha 43 | ) 44 | CONSOLE_LOGGER.info( 45 | f"New branch {target_branch} created in repository {self.repo.name}" 46 | ) 47 | 48 | # Read input files from the local directory 49 | files = [] 50 | for file_name in os.listdir(input_dir): 51 | file_path = os.path.join(input_dir, file_name) 52 | with open(file_path, "r") as f: 53 | file_content = f.read() 54 | files.append({"name": file_name, "content": file_content}) 55 | 56 | has_changes = False 57 | # Compare base layer files 58 | for file in files: 59 | output_path = output_dir + "/" + file["name"] 60 | contents = self.repo.get_contents(output_dir, ref=target_branch) 61 | 62 | if file["name"] in [content_file.name for content_file in contents]: 63 | if file_update_message: 64 | message = file_update_message 65 | else: 66 | message = f"update {file['name']}" 67 | content_file = self.repo.get_contents(output_path, ref=target_branch) 68 | # Compare file contents 69 | if content_file.decoded_content.decode() == file["content"]: 70 | CONSOLE_LOGGER.info( 71 | f"File {file['name']} already exists and it is up to date" 72 | ) 73 | continue 74 | has_changes = True 75 | self.repo.update_file( 76 | path=output_path, 77 | message=message, 78 | content=file["content"], 79 | sha=content_file.sha, 80 | branch=target_branch, 81 | author=author, 82 | ) 83 | CONSOLE_LOGGER.info(f"File {file['name']} has been updated") 84 | continue 85 | 86 | else: 87 | has_changes = True 88 | if file_creation_message: 89 | message = file_creation_message 90 | else: 91 | message = f"create {file['name']}" 92 | self.repo.create_file( 93 | path=output_path, 94 | message=message, 95 | content=file["content"], 96 | branch=target_branch, 97 | committer=author, 98 | author=author, 99 | ) 100 | CONSOLE_LOGGER.info(f"File {file['name']} has been created") 101 | 102 | # Return if there are no changes 103 | if not has_changes: 104 | CONSOLE_LOGGER.info( 105 | "No changes detected. No commits have been made to the repository" 106 | ) 107 | return 108 | 109 | def create_pull_request( 110 | self, base_branch: str, target_branch: str, pr_title: str, pr_body: str 111 | ): 112 | # Create a pull request if it does not exist 113 | pulls = self.repo.get_pulls(state="open", sort="created", base=base_branch) 114 | if target_branch in [pull.head.ref for pull in pulls]: 115 | CONSOLE_LOGGER.info( 116 | f"Pull request already exists for branch {target_branch}" 117 | ) 118 | return 119 | 120 | # Create a new pull request 121 | pull_request = self.repo.create_pull( 122 | title=pr_title, 123 | body=pr_body, 124 | base=base_branch, 125 | head=f"{target_branch}", 126 | draft=True, 127 | ) 128 | CONSOLE_LOGGER.info(f"Pull request created: {pull_request.html_url}") 129 | 130 | def delete_branch(self, branch_name: str): 131 | try: 132 | ref = self.repo.get_git_ref(f"heads/{branch_name}") 133 | ref.delete() 134 | CONSOLE_LOGGER.info(f"Branch {branch_name} deleted") 135 | except UnknownObjectException: 136 | CONSOLE_LOGGER.warn(f"{branch_name} does not exist") 137 | --------------------------------------------------------------------------------