├── .circleci └── config.yml ├── .github └── FUNDING.yml ├── .gitignore ├── API.md ├── LICENSE ├── Makefile ├── README.md ├── img ├── td-cli.gif └── td-cli.png ├── poetry.lock ├── pyproject.toml ├── setup.cfg ├── setup.py └── todo ├── __init__.py ├── __main__.py ├── commands ├── __init__.py ├── base.py ├── config │ ├── __init__.py │ └── initialize.py ├── group │ ├── __init__.py │ ├── add.py │ ├── delete.py │ ├── get.py │ ├── list.py │ └── preset.py └── todo │ ├── __init__.py │ ├── add.py │ ├── complete.py │ ├── count.py │ ├── delete.py │ ├── edit.py │ ├── get.py │ ├── list.py │ ├── list_interactive.py │ └── uncomplete.py ├── constants.py ├── exceptions.py ├── parser ├── __init__.py ├── base.py └── subparsers │ ├── __init__.py │ ├── add_group.py │ ├── add_todo.py │ ├── count_todos.py │ ├── group.py │ ├── initialize_config.py │ ├── list_groups.py │ ├── list_todos.py │ └── todo.py ├── renderers ├── __init__.py ├── base.py ├── render_error.py ├── render_help.py ├── render_input.py ├── render_output.py ├── render_output_with_textwrap.py └── styles │ ├── __init__.py │ ├── ansi_fore.py │ ├── ansi_style.py │ └── base.py ├── services ├── __init__.py ├── base.py ├── group.py └── todo.py ├── settings.py └── utils ├── __init__.py └── menu ├── __init__.py ├── horizontal_tracker.py └── vertical_tracker.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | description: | 3 | Will install dev and packaging extra dependencies from 4 | setup.py. 5 | 6 | workflows: 7 | version: 2.1 8 | lint: 9 | jobs: 10 | - build 11 | - flake8: 12 | requires: 13 | - build 14 | - black: 15 | requires: 16 | - build 17 | - isort: 18 | requires: 19 | - build 20 | - mypy: 21 | requires: 22 | - build 23 | 24 | executors: 25 | python: 26 | description: Python image build image 27 | parameters: 28 | version: 29 | description: Python version to use 30 | type: string 31 | default: "3.11" 32 | docker: 33 | - image: cimg/python:<> 34 | environment: 35 | POETRY_VIRTUALENVS_PATH: /home/circleci/td-cli/.cache 36 | 37 | references: 38 | container_config: &container_config 39 | working_directory: ~/td-cli 40 | executor: 41 | name: python 42 | version: <> 43 | parameters: 44 | python-version: 45 | description: Python version to use 46 | type: string 47 | default: "3.11" 48 | cache-version: 49 | description: A cache version that may be used for cache busting 50 | type: string 51 | default: "v2" 52 | 53 | commands: 54 | # Use a venv so we don't have to run as root to make caches work 55 | setup_venv_and_install_deps: 56 | description: | 57 | Setup a python venv for the project. 58 | Install project dependencies into a virtual environment. 59 | 60 | Installs the dependencies declared in pyproject.toml, including all 61 | dev dependencies. 62 | parameters: 63 | cache-version: 64 | description: A cache version that may be used for cache busting 65 | type: string 66 | default: "v2" 67 | steps: 68 | - restore_cache: 69 | key: <>-poetry-cache-{{ checksum "poetry.lock" }} 70 | - run: make requirements 71 | - save_cache: 72 | key: <>-poetry-cache-{{ checksum "poetry.lock" }} 73 | paths: 74 | - .cache 75 | 76 | jobs: 77 | build: 78 | description: Build and install dependencies 79 | <<: *container_config 80 | steps: 81 | - checkout 82 | - setup_venv_and_install_deps: 83 | cache-version: <> 84 | - persist_to_workspace: 85 | root: ~/td-cli 86 | paths: 87 | - .cache 88 | - Makefile 89 | - todo 90 | - pyproject.toml 91 | - setup.cfg 92 | 93 | isort: 94 | description: Execute import order checks with isort 95 | <<: *container_config 96 | steps: 97 | - attach_workspace: 98 | at: ~/td-cli 99 | - run: make isort 100 | 101 | black: 102 | description: Execute code format checks with black 103 | <<: *container_config 104 | steps: 105 | - attach_workspace: 106 | at: ~/td-cli 107 | - run: make black 108 | 109 | flake8: 110 | description: Execute code format checks with flake8 111 | <<: *container_config 112 | steps: 113 | - attach_workspace: 114 | at: ~/td-cli 115 | - run: make flake8 116 | 117 | mypy: 118 | description: Execute code format checks with mypy 119 | <<: *container_config 120 | steps: 121 | - attach_workspace: 122 | at: ~/td-cli 123 | - run: make mypy 124 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: darrikonn 2 | github: darrikonn 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | *.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 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # tmp/ 108 | todo.db 109 | 110 | # Other 111 | .DS_Store 112 | designs/ 113 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # td 2 | ``` 3 | usage: td {add,add-group,[id],group,list,list-groups} ... 4 | 5 | positional arguments: 6 | {...} commands 7 | add (a) add todo 8 | add-group (ag) add group 9 | [id] manage todo 10 | group (g) manage group 11 | list (ls, l) list todos *DEFAULT* 12 | list-groups (lg, lsg) list groups 13 | 14 | optional arguments: 15 | -h, --help show this help message and exit 16 | --completed, -c filter by completed todos 17 | --uncompleted, -u filter by uncompleted todos 18 | --raw, -r only show todos 19 | --group GROUP, -g GROUP 20 | filter by name of group 21 | --interactive, -i toggle interactive mode 22 | ``` 23 | `td` defaults to `td list` 24 | 25 |
26 | 27 | ## List todos 28 | ``` 29 | usage: td [--completed] [--uncompleted] [--raw] [--group GROUP] [--interactive] 30 | td list [-c] [-u] [-r] [-g GROUP] [-i] 31 | td ls [-c] [-u] [-r] [-g GROUP] [-i] 32 | td l [-c] [-u] [-r] [-g GROUP] [-i] 33 | 34 | optional arguments: 35 | -h, --help show this help message and exit 36 | --completed, -c filter by completed todos 37 | --uncompleted, -u filter by uncompleted todos 38 | --raw, -r only show todos 39 | --group GROUP, -g GROUP 40 | filter by name of group 41 | --interactive, -i toggle interactive mode 42 | ``` 43 | `td` is the shortcut to `td list` 44 | 45 | #### Examples 46 | 1. `td list --completed` 47 | 2. `td -c` 48 | 3. `td --interactive` 49 | 50 |
51 | 52 | ## Add a todo 53 | ``` 54 | usage: td add [name] [--complete] [--uncomplete] [--group GROUP] [--edit | --details DETAILS] 55 | td a [name] [-c] [-u] [-g GROUP] [-e | -d DETAILS] 56 | 57 | positional arguments: 58 | name the new todo's name 59 | 60 | optional arguments: 61 | -h, --help show this help message and exit 62 | --complete, -c complete todo 63 | --uncomplete, -u uncomplete todo 64 | --group GROUP, -g GROUP 65 | name of todo's group 66 | --edit, -e edit the todo's details in your editor 67 | --details DETAILS, -d DETAILS 68 | the todo's details 69 | ``` 70 | `--edit` and `--details` are mutually exclusive. 71 | 72 | #### Examples 73 | 1. `td add 'my new todo' --edit` 74 | 2. `td a 'my new todo' -d 'the details'` 75 | 76 |
77 | 78 | ## Manage todo 79 | ``` 80 | usage: td [id] {get,delete,uncomplete,complete,edit} ... 81 | 82 | positional arguments: 83 | id the id of the todo {...} commands 84 | get (g) show todo's details 85 | delete (d) delete todo 86 | uncomplete (u) uncomplete todo 87 | complete (c) complete todo 88 | edit (e) edit todo 89 | 90 | optional arguments: 91 | -h, --help show this help message and exit 92 | ``` 93 | `td [id]` defaults to `td [id] get` 94 | You don't have to specify the whole `id`, a substring will do 95 | 96 | ### Get todo's details 97 | ``` 98 | usage: td [id] 99 | td [id] get 100 | td [id] g 101 | 102 | optional arguments: 103 | -h, --help show this help message and exit 104 | ``` 105 | `td [id]` is the shortcut to `td [id] get` 106 | 107 | ### Delete todo 108 | ``` 109 | usage: td [id] delete [-yes] 110 | td [id] d [-y] 111 | 112 | optional arguments: 113 | -h, --help show this help message and exit 114 | --yes, -y skip yes/no prompt when deleting todo 115 | ``` 116 | 117 | ### Complete todo 118 | ``` 119 | usage: td [id] complete 120 | td [id] c 121 | 122 | optional arguments: 123 | -h, --help show this help message and exit 124 | ``` 125 | 126 | ### Uncomplete todo 127 | ``` 128 | usage: td [id] uncomplete 129 | td [id] u 130 | 131 | optional arguments: 132 | -h, --help show this help message and exit 133 | ``` 134 | 135 | ### Edit todo 136 | ``` 137 | usage: td [id] edit [--name NAME] [--details DETAILS] 138 | td [id] e [-n NAME] [-d DETAILS] 139 | 140 | optional arguments: 141 | -h, --help show this help message and exit 142 | --name NAME, -n NAME update todo's name 143 | --details DETAILS, -d DETAILS 144 | update todo's detail 145 | --group GROUP, -g GROUP 146 | set todo's group 147 | ``` 148 | If no optional arguments are provided, the todo will be 149 | opened in your editor where you can edit the todo's details. 150 | 151 | #### Examples 152 | 1. `td 1337` 153 | 2. `td 1337 complete` 154 | 3. `td 1337 u` 155 | 4. `td 1337 edit -n "new name" -d "details"` 156 | 157 | 158 |
159 | 160 | ## Initialize configuration 161 | ``` 162 | usage: td init-config 163 | td ic 164 | 165 | initialize config 166 | 167 | optional arguments: 168 | -h, --help show this help message and exit 169 | ``` 170 | 171 |
172 | 173 | ## List groups 174 | ``` 175 | usage: td list-groups [--completed] [--uncompleted] 176 | td lg [-c] [-u] 177 | td lsg [-c] [-u] 178 | 179 | optional arguments: 180 | -h, --help show this help message and exit 181 | --completed, -c filter by completed groups 182 | --uncompleted, -u filter by uncompleted groups 183 | ``` 184 | 185 |
186 | 187 | ## Add a group 188 | ``` 189 | usage: td add-group [name] 190 | td ag [name] 191 | 192 | positional arguments: 193 | name the new group's name 194 | 195 | optional arguments: 196 | -h, --help show this help message and exit 197 | ``` 198 | 199 |
200 | 201 | ## Manage group 202 | ``` 203 | usage: td group [name] {list,delete,preset} ... 204 | td g [name] {l,d,p} ... 205 | 206 | positional arguments: 207 | name name of the group 208 | {...} commands 209 | list (ls, l) list group's todos 210 | delete (d) delete group and its todos 211 | preset (p) set group as the default group when listing todos 212 | 213 | optional arguments: 214 | -h, --help show this help message and exit 215 | ``` 216 | `td group [name]` defaults to `td group [name] list` 217 | 218 | ### List group's todos 219 | ``` 220 | usage: td group [name] 221 | td group [name] list 222 | td group [name] ls 223 | td group [name] l 224 | 225 | optional arguments: 226 | -h, --help show this help message and exit 227 | --completed, -c filter by completed todos 228 | --uncompleted, -u filter by uncompleted todos 229 | --interactive, -i toggle interactive mode 230 | ``` 231 | `td group [name]` is the shortcut to `td group [name] get` 232 | 233 | ### Delete group and its todos 234 | ``` 235 | usage: td group [name] delete [--yes] 236 | td group [name] d [-y] 237 | 238 | optional arguments: 239 | -h, --help show this help message and exit 240 | --yes, -y skip yes/no prompt when deleting group 241 | ``` 242 | 243 | ### Set group as default when listing todos 244 | ``` 245 | usage: td group [name] preset 246 | td group [name] p 247 | 248 | optional arguments: 249 | -h, --help show this help message and exit 250 | ``` 251 | 252 | #### Examples 253 | 1. `td group my-project` 254 | 2. `td g my-project --completed` 255 | 3. `td g my-project preset` 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Darri Steinn Konráðsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ### VARIABLES ### 2 | 3 | RUN=poetry run 4 | DATE=`date +%Y-%m-%d` 5 | CODE_STYLE_FILE_LIST=todo 6 | 7 | ARGS=$(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 8 | $(eval $(ARGS):;@:) 9 | 10 | 11 | ### TARGETS ### 12 | 13 | # Install poetry 14 | poetry: 15 | pip3 install poetry 16 | 17 | # Install requirements 18 | requirements: 19 | poetry install --no-root 20 | 21 | deploy_requirements: 22 | ${RUN} pip install wheel 23 | 24 | # Install poetry and requirements 25 | dev: poetry requirements 26 | 27 | # Spin up shell in virtual environment 28 | venv: 29 | poetry shell 30 | 31 | # Lint using flake8 32 | flake8: 33 | ${RUN} flake8 ${CODE_STYLE_FILE_LIST} 34 | 35 | # Lint using black 36 | black: 37 | ${RUN} black --target-version py38 --check ${CODE_STYLE_FILE_LIST} 38 | 39 | # Format using black 40 | black_format: 41 | ${RUN} black --target-version py38 ${CODE_STYLE_FILE_LIST} 42 | 43 | # Lint using isort 44 | isort: 45 | ${RUN} isort -c -rc ${CODE_STYLE_FILE_LIST} 46 | 47 | # Format using isort 48 | isort_format: 49 | ${RUN} isort -rc ${CODE_STYLE_FILE_LIST} 50 | 51 | # Lint using mypy 52 | mypy: 53 | ${RUN} mypy ${CODE_STYLE_FILE_LIST} 54 | 55 | # Lint using all methods combined 56 | lint: flake8 black isort mypy 57 | 58 | # Format using all methods combined 59 | format: black_format isort_format 60 | 61 | clean: 62 | -rm -r dist build td_cli.egg-info 2> /dev/null 63 | 64 | build: 65 | ${RUN} python setup.py sdist bdist_wheel 66 | 67 | upload_test: 68 | ${RUN} twine upload --repository-url https://test.pypi.org/legacy/ dist/* 69 | 70 | upload: 71 | ${RUN} twine upload --repository td-cli dist/* 72 | 73 | install_test: 74 | ${RUN} python -m pip install --index-url https://test.pypi.org/simple/ td-cli 75 | 76 | install: 77 | ${RUN} python -m pip install td-cli 78 | 79 | publish_test: clean build upload_test install_test 80 | 81 | publish: clean build upload install 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Icon 3 |

4 | 5 |

6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |

 27 |   

td-cli is a command line todo manager,
where you can organize and manage your todos across multiple projects

28 |

gif

29 |

30 |
31 | 32 | ## Installation 33 | 34 | [**td-cli**](https://pypi.org/project/td-cli/) only works for `python 3`, so it needs to be installed with `pip3` 35 | 36 | ```bash 37 | pip3 install td-cli 38 | ``` 39 | 40 | ### Windows 10 41 | 42 | In order to use the interactive mode on Windows, you'll have to install [**windows-curses**](https://pypi.org/project/windows-curses/) 43 | 44 | ```bash 45 | pip install windows-curses 46 | ``` 47 | 48 | In addition to that, [**Windows Terminal**](https://github.com/microsoft/terminal) is recommended for better UX. 49 | 50 | ## Getting started 51 | 52 | Run `td --help` to see possible commands. 53 | 54 | Here are some to get you started: 55 | 56 | - Run `td` to list all your todos. 57 | 58 | - Run `td add "my new awesome todo"` to add a new todo. 59 | 60 | - Run `td complete` to complete your todo. You don't have to specify the whole `id`, a substring will do. It'll fetch the first one that it finds in the same order as when you list your todos. 61 | 62 | Note that `global` is a preserved group name where you can list all your global groups. You can always set it as the default with: 63 | 64 | ```bash 65 | td group global preset 66 | ``` 67 | 68 | ## API 69 | 70 | Check out the [`api`](https://github.com/darrikonn/td-cli/blob/master/API.md). 71 | 72 | ## Configuring 73 | 74 | The location of your todos and your configuration will depend on these environment variables (in this order): 75 | 76 | 1. **TD_CLI_HOME**: determines where your `todo.db` and `todo.cfg` file will live 77 | 2. **XDG_CONFIG_HOME**: a fallback if `$TD_CLI_HOME` is not set 78 | 3. **HOME**: a fallback if `$XDG_CONFIG_HOME` is not set. If `$HOME` is used; all files will be transformed to a dotfile, i.e.`~/.todo.db` and `~/.todo.cfg`. 79 | 80 | ### Database name 81 | 82 | Your database instance will be located in in the before-mentioned configuration directory. 83 | By default the database will be named `todo`. 84 | 85 | You can change your database name by specifying `database_name` in your `$TD_CLI_HOME/.todo.cfg` file: 86 | 87 | ```cfg 88 | [settings] 89 | database_name: something_else 90 | ``` 91 | 92 | This results in a database instance at `$TD_CLI_HOME/.something_else.db` 93 | 94 | ### Format 95 | 96 | You can specify your preferred format of your todo's details via the format config keyword: 97 | 98 | ```cfg 99 | format: md 100 | ``` 101 | 102 | This would result in the `.md` (Markdown) file extension when editing a todo. This allows you to use the power of your editor to e.g. syntax highlight the details, and etc. 103 | 104 | ### Editor 105 | 106 | When editing a todo, `td edit`, you can both specify the todo's `name` and the todo's `details` via options (see `td edit --help`). If no option is specified, your todo will be opened in `vi` by default (your `environement EDITOR` will override this) where you can edit the todo's details. You can change the default editor by updating your config: 107 | 108 | ```cfg 109 | [settings] 110 | editor: nvim 111 | ``` 112 | 113 | ### Only list uncompleted todos 114 | 115 | When listing todos, by default td-cli will list both completed and uncompleted todos. If you want to only list uncompleted todos by default, then you can apply the completed config: 116 | 117 | ```cfg 118 | [settings] 119 | completed: 0 120 | ``` 121 | 122 | ### Group 123 | 124 | When listing todos, you have the option of specifying what group to list from: 125 | 126 | ```bash 127 | td -g my-group 128 | # or 129 | td g my-group 130 | ``` 131 | 132 | If no group is provided, `td` will list from the current default group. You can globally set the default group with: 133 | 134 | ```bash 135 | td g my-group preset 136 | ``` 137 | 138 | However, there's an option to set the default group per git project (this is not possible from the root config `$TD_CLI_HOME/.todo.cfg`). 139 | In any root of your projects, you can create a `.td.cfg` config file to specify what group to default on (this will override the global default group): 140 | 141 | ```cfg 142 | [settings] 143 | group: my-group 144 | ``` 145 | 146 | If you run `td` within your git project, td will default to _my-group_. 147 | 148 | I recommend globally ignoring `.td.cfg` in `~/.gitignore`. 149 | -------------------------------------------------------------------------------- /img/td-cli.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrikonn/td-cli/e6efb70293499cee030ce689ff8c81bc11407d38/img/td-cli.gif -------------------------------------------------------------------------------- /img/td-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrikonn/td-cli/e6efb70293499cee030ce689ff8c81bc11407d38/img/td-cli.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asttokens" 5 | version = "2.4.1" 6 | description = "Annotate AST trees with source code positions" 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, 11 | {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, 12 | ] 13 | 14 | [package.dependencies] 15 | six = ">=1.12.0" 16 | 17 | [package.extras] 18 | astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] 19 | test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] 20 | 21 | [[package]] 22 | name = "black" 23 | version = "24.3.0" 24 | description = "The uncompromising code formatter." 25 | optional = false 26 | python-versions = ">=3.8" 27 | files = [ 28 | {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, 29 | {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, 30 | {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, 31 | {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, 32 | {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, 33 | {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, 34 | {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, 35 | {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, 36 | {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, 37 | {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, 38 | {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, 39 | {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, 40 | {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, 41 | {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, 42 | {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, 43 | {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, 44 | {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, 45 | {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, 46 | {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, 47 | {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, 48 | {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, 49 | {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, 50 | ] 51 | 52 | [package.dependencies] 53 | click = ">=8.0.0" 54 | mypy-extensions = ">=0.4.3" 55 | packaging = ">=22.0" 56 | pathspec = ">=0.9.0" 57 | platformdirs = ">=2" 58 | 59 | [package.extras] 60 | colorama = ["colorama (>=0.4.3)"] 61 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 62 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 63 | uvloop = ["uvloop (>=0.15.2)"] 64 | 65 | [[package]] 66 | name = "certifi" 67 | version = "2024.7.4" 68 | description = "Python package for providing Mozilla's CA Bundle." 69 | optional = false 70 | python-versions = ">=3.6" 71 | files = [ 72 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 73 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 74 | ] 75 | 76 | [[package]] 77 | name = "cffi" 78 | version = "1.16.0" 79 | description = "Foreign Function Interface for Python calling C code." 80 | optional = false 81 | python-versions = ">=3.8" 82 | files = [ 83 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 84 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 85 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 86 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 87 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 88 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 89 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 90 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 91 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 92 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 93 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 94 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 95 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 96 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 97 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 98 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 99 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 100 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 101 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 102 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 103 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 104 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 105 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 106 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 107 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 108 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 109 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 110 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 111 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 112 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 113 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 114 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 115 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 116 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 117 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 118 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 119 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 120 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 121 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 122 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 123 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 124 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 125 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 126 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 127 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 128 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 129 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 130 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 131 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 132 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 133 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 134 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 135 | ] 136 | 137 | [package.dependencies] 138 | pycparser = "*" 139 | 140 | [[package]] 141 | name = "charset-normalizer" 142 | version = "3.3.2" 143 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 144 | optional = false 145 | python-versions = ">=3.7.0" 146 | files = [ 147 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 148 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 149 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 150 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 151 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 152 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 153 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 154 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 155 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 156 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 157 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 158 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 159 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 160 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 161 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 162 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 163 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 164 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 165 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 166 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 167 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 168 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 169 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 170 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 171 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 172 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 173 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 174 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 175 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 176 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 177 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 178 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 179 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 180 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 181 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 182 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 183 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 184 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 185 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 186 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 187 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 188 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 189 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 190 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 191 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 192 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 193 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 194 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 195 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 196 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 197 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 198 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 199 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 200 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 201 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 202 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 203 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 204 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 205 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 206 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 207 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 208 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 209 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 210 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 211 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 212 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 213 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 214 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 215 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 216 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 217 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 218 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 219 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 220 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 221 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 222 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 223 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 224 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 225 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 226 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 227 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 228 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 229 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 230 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 231 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 232 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 233 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 234 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 235 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 236 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 237 | ] 238 | 239 | [[package]] 240 | name = "click" 241 | version = "8.1.7" 242 | description = "Composable command line interface toolkit" 243 | optional = false 244 | python-versions = ">=3.7" 245 | files = [ 246 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 247 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 248 | ] 249 | 250 | [package.dependencies] 251 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 252 | 253 | [[package]] 254 | name = "colorama" 255 | version = "0.4.6" 256 | description = "Cross-platform colored terminal text." 257 | optional = false 258 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 259 | files = [ 260 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 261 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 262 | ] 263 | 264 | [[package]] 265 | name = "cryptography" 266 | version = "44.0.1" 267 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 268 | optional = false 269 | python-versions = "!=3.9.0,!=3.9.1,>=3.7" 270 | files = [ 271 | {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"}, 272 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"}, 273 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"}, 274 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"}, 275 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"}, 276 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"}, 277 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"}, 278 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"}, 279 | {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"}, 280 | {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"}, 281 | {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"}, 282 | {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"}, 283 | {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"}, 284 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"}, 285 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"}, 286 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"}, 287 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"}, 288 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"}, 289 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"}, 290 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"}, 291 | {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"}, 292 | {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"}, 293 | {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"}, 294 | {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"}, 295 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"}, 296 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"}, 297 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"}, 298 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"}, 299 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"}, 300 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"}, 301 | {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"}, 302 | ] 303 | 304 | [package.dependencies] 305 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 306 | 307 | [package.extras] 308 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] 309 | docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] 310 | nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] 311 | pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] 312 | sdist = ["build (>=1.0.0)"] 313 | ssh = ["bcrypt (>=3.1.5)"] 314 | test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] 315 | test-randomorder = ["pytest-randomly"] 316 | 317 | [[package]] 318 | name = "decorator" 319 | version = "5.1.1" 320 | description = "Decorators for Humans" 321 | optional = false 322 | python-versions = ">=3.5" 323 | files = [ 324 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 325 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 326 | ] 327 | 328 | [[package]] 329 | name = "docutils" 330 | version = "0.20.1" 331 | description = "Docutils -- Python Documentation Utilities" 332 | optional = false 333 | python-versions = ">=3.7" 334 | files = [ 335 | {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, 336 | {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, 337 | ] 338 | 339 | [[package]] 340 | name = "executing" 341 | version = "2.0.1" 342 | description = "Get the currently executing AST node of a frame, and other information" 343 | optional = false 344 | python-versions = ">=3.5" 345 | files = [ 346 | {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, 347 | {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, 348 | ] 349 | 350 | [package.extras] 351 | tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] 352 | 353 | [[package]] 354 | name = "flake8" 355 | version = "6.1.0" 356 | description = "the modular source code checker: pep8 pyflakes and co" 357 | optional = false 358 | python-versions = ">=3.8.1" 359 | files = [ 360 | {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, 361 | {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, 362 | ] 363 | 364 | [package.dependencies] 365 | mccabe = ">=0.7.0,<0.8.0" 366 | pycodestyle = ">=2.11.0,<2.12.0" 367 | pyflakes = ">=3.1.0,<3.2.0" 368 | 369 | [[package]] 370 | name = "idna" 371 | version = "3.7" 372 | description = "Internationalized Domain Names in Applications (IDNA)" 373 | optional = false 374 | python-versions = ">=3.5" 375 | files = [ 376 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 377 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 378 | ] 379 | 380 | [[package]] 381 | name = "importlib-metadata" 382 | version = "6.8.0" 383 | description = "Read metadata from Python packages" 384 | optional = false 385 | python-versions = ">=3.8" 386 | files = [ 387 | {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, 388 | {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, 389 | ] 390 | 391 | [package.dependencies] 392 | zipp = ">=0.5" 393 | 394 | [package.extras] 395 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 396 | perf = ["ipython"] 397 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 398 | 399 | [[package]] 400 | name = "ipdb" 401 | version = "0.13.13" 402 | description = "IPython-enabled pdb" 403 | optional = false 404 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 405 | files = [ 406 | {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, 407 | {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, 408 | ] 409 | 410 | [package.dependencies] 411 | decorator = {version = "*", markers = "python_version >= \"3.11\""} 412 | ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""} 413 | 414 | [[package]] 415 | name = "ipython" 416 | version = "8.18.1" 417 | description = "IPython: Productive Interactive Computing" 418 | optional = false 419 | python-versions = ">=3.9" 420 | files = [ 421 | {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, 422 | {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, 423 | ] 424 | 425 | [package.dependencies] 426 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 427 | decorator = "*" 428 | jedi = ">=0.16" 429 | matplotlib-inline = "*" 430 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 431 | prompt-toolkit = ">=3.0.41,<3.1.0" 432 | pygments = ">=2.4.0" 433 | stack-data = "*" 434 | traitlets = ">=5" 435 | 436 | [package.extras] 437 | all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] 438 | black = ["black"] 439 | doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] 440 | kernel = ["ipykernel"] 441 | nbconvert = ["nbconvert"] 442 | nbformat = ["nbformat"] 443 | notebook = ["ipywidgets", "notebook"] 444 | parallel = ["ipyparallel"] 445 | qtconsole = ["qtconsole"] 446 | test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] 447 | test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] 448 | 449 | [[package]] 450 | name = "isort" 451 | version = "5.12.0" 452 | description = "A Python utility / library to sort Python imports." 453 | optional = false 454 | python-versions = ">=3.8.0" 455 | files = [ 456 | {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, 457 | {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, 458 | ] 459 | 460 | [package.extras] 461 | colors = ["colorama (>=0.4.3)"] 462 | pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] 463 | plugins = ["setuptools"] 464 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 465 | 466 | [[package]] 467 | name = "jaraco-classes" 468 | version = "3.3.0" 469 | description = "Utility functions for Python class constructs" 470 | optional = false 471 | python-versions = ">=3.8" 472 | files = [ 473 | {file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"}, 474 | {file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"}, 475 | ] 476 | 477 | [package.dependencies] 478 | more-itertools = "*" 479 | 480 | [package.extras] 481 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 482 | testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 483 | 484 | [[package]] 485 | name = "jedi" 486 | version = "0.19.1" 487 | description = "An autocompletion tool for Python that can be used for text editors." 488 | optional = false 489 | python-versions = ">=3.6" 490 | files = [ 491 | {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, 492 | {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, 493 | ] 494 | 495 | [package.dependencies] 496 | parso = ">=0.8.3,<0.9.0" 497 | 498 | [package.extras] 499 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 500 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 501 | testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 502 | 503 | [[package]] 504 | name = "jeepney" 505 | version = "0.8.0" 506 | description = "Low-level, pure Python DBus protocol wrapper." 507 | optional = false 508 | python-versions = ">=3.7" 509 | files = [ 510 | {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, 511 | {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, 512 | ] 513 | 514 | [package.extras] 515 | test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] 516 | trio = ["async_generator", "trio"] 517 | 518 | [[package]] 519 | name = "keyring" 520 | version = "24.3.0" 521 | description = "Store and access your passwords safely." 522 | optional = false 523 | python-versions = ">=3.8" 524 | files = [ 525 | {file = "keyring-24.3.0-py3-none-any.whl", hash = "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836"}, 526 | {file = "keyring-24.3.0.tar.gz", hash = "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25"}, 527 | ] 528 | 529 | [package.dependencies] 530 | importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} 531 | "jaraco.classes" = "*" 532 | jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} 533 | pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} 534 | SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} 535 | 536 | [package.extras] 537 | completion = ["shtab (>=1.1.0)"] 538 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] 539 | testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 540 | 541 | [[package]] 542 | name = "markdown-it-py" 543 | version = "3.0.0" 544 | description = "Python port of markdown-it. Markdown parsing, done right!" 545 | optional = false 546 | python-versions = ">=3.8" 547 | files = [ 548 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 549 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 550 | ] 551 | 552 | [package.dependencies] 553 | mdurl = ">=0.1,<1.0" 554 | 555 | [package.extras] 556 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 557 | code-style = ["pre-commit (>=3.0,<4.0)"] 558 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 559 | linkify = ["linkify-it-py (>=1,<3)"] 560 | plugins = ["mdit-py-plugins"] 561 | profiling = ["gprof2dot"] 562 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 563 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 564 | 565 | [[package]] 566 | name = "matplotlib-inline" 567 | version = "0.1.6" 568 | description = "Inline Matplotlib backend for Jupyter" 569 | optional = false 570 | python-versions = ">=3.5" 571 | files = [ 572 | {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, 573 | {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, 574 | ] 575 | 576 | [package.dependencies] 577 | traitlets = "*" 578 | 579 | [[package]] 580 | name = "mccabe" 581 | version = "0.7.0" 582 | description = "McCabe checker, plugin for flake8" 583 | optional = false 584 | python-versions = ">=3.6" 585 | files = [ 586 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 587 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 588 | ] 589 | 590 | [[package]] 591 | name = "mdurl" 592 | version = "0.1.2" 593 | description = "Markdown URL utilities" 594 | optional = false 595 | python-versions = ">=3.7" 596 | files = [ 597 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 598 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 599 | ] 600 | 601 | [[package]] 602 | name = "more-itertools" 603 | version = "10.1.0" 604 | description = "More routines for operating on iterables, beyond itertools" 605 | optional = false 606 | python-versions = ">=3.8" 607 | files = [ 608 | {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, 609 | {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, 610 | ] 611 | 612 | [[package]] 613 | name = "mypy" 614 | version = "1.7.1" 615 | description = "Optional static typing for Python" 616 | optional = false 617 | python-versions = ">=3.8" 618 | files = [ 619 | {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, 620 | {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, 621 | {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, 622 | {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, 623 | {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, 624 | {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, 625 | {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, 626 | {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, 627 | {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, 628 | {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, 629 | {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, 630 | {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, 631 | {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, 632 | {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, 633 | {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, 634 | {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, 635 | {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, 636 | {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, 637 | {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, 638 | {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, 639 | {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, 640 | {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, 641 | {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, 642 | {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, 643 | {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, 644 | {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, 645 | {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, 646 | ] 647 | 648 | [package.dependencies] 649 | mypy-extensions = ">=1.0.0" 650 | typing-extensions = ">=4.1.0" 651 | 652 | [package.extras] 653 | dmypy = ["psutil (>=4.0)"] 654 | install-types = ["pip"] 655 | mypyc = ["setuptools (>=50)"] 656 | reports = ["lxml"] 657 | 658 | [[package]] 659 | name = "mypy-extensions" 660 | version = "1.0.0" 661 | description = "Type system extensions for programs checked with the mypy type checker." 662 | optional = false 663 | python-versions = ">=3.5" 664 | files = [ 665 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 666 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 667 | ] 668 | 669 | [[package]] 670 | name = "nh3" 671 | version = "0.2.14" 672 | description = "Ammonia HTML sanitizer Python binding" 673 | optional = false 674 | python-versions = "*" 675 | files = [ 676 | {file = "nh3-0.2.14-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:9be2f68fb9a40d8440cbf34cbf40758aa7f6093160bfc7fb018cce8e424f0c3a"}, 677 | {file = "nh3-0.2.14-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f99212a81c62b5f22f9e7c3e347aa00491114a5647e1f13bbebd79c3e5f08d75"}, 678 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7771d43222b639a4cd9e341f870cee336b9d886de1ad9bec8dddab22fe1de450"}, 679 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:525846c56c2bcd376f5eaee76063ebf33cf1e620c1498b2a40107f60cfc6054e"}, 680 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e8986f1dd3221d1e741fda0a12eaa4a273f1d80a35e31a1ffe579e7c621d069e"}, 681 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18415df36db9b001f71a42a3a5395db79cf23d556996090d293764436e98e8ad"}, 682 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:377aaf6a9e7c63962f367158d808c6a1344e2b4f83d071c43fbd631b75c4f0b2"}, 683 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0be5c792bd43d0abef8ca39dd8acb3c0611052ce466d0401d51ea0d9aa7525"}, 684 | {file = "nh3-0.2.14-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93a943cfd3e33bd03f77b97baa11990148687877b74193bf777956b67054dcc6"}, 685 | {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac8056e937f264995a82bf0053ca898a1cb1c9efc7cd68fa07fe0060734df7e4"}, 686 | {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:203cac86e313cf6486704d0ec620a992c8bc164c86d3a4fd3d761dd552d839b5"}, 687 | {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:5529a3bf99402c34056576d80ae5547123f1078da76aa99e8ed79e44fa67282d"}, 688 | {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aed56a86daa43966dd790ba86d4b810b219f75b4bb737461b6886ce2bde38fd6"}, 689 | {file = "nh3-0.2.14-cp37-abi3-win32.whl", hash = "sha256:116c9515937f94f0057ef50ebcbcc10600860065953ba56f14473ff706371873"}, 690 | {file = "nh3-0.2.14-cp37-abi3-win_amd64.whl", hash = "sha256:88c753efbcdfc2644a5012938c6b9753f1c64a5723a67f0301ca43e7b85dcf0e"}, 691 | {file = "nh3-0.2.14.tar.gz", hash = "sha256:a0c509894fd4dccdff557068e5074999ae3b75f4c5a2d6fb5415e782e25679c4"}, 692 | ] 693 | 694 | [[package]] 695 | name = "packaging" 696 | version = "23.2" 697 | description = "Core utilities for Python packages" 698 | optional = false 699 | python-versions = ">=3.7" 700 | files = [ 701 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 702 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 703 | ] 704 | 705 | [[package]] 706 | name = "parso" 707 | version = "0.8.3" 708 | description = "A Python Parser" 709 | optional = false 710 | python-versions = ">=3.6" 711 | files = [ 712 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 713 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 714 | ] 715 | 716 | [package.extras] 717 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 718 | testing = ["docopt", "pytest (<6.0.0)"] 719 | 720 | [[package]] 721 | name = "pathspec" 722 | version = "0.11.2" 723 | description = "Utility library for gitignore style pattern matching of file paths." 724 | optional = false 725 | python-versions = ">=3.7" 726 | files = [ 727 | {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, 728 | {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, 729 | ] 730 | 731 | [[package]] 732 | name = "pexpect" 733 | version = "4.9.0" 734 | description = "Pexpect allows easy control of interactive console applications." 735 | optional = false 736 | python-versions = "*" 737 | files = [ 738 | {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, 739 | {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, 740 | ] 741 | 742 | [package.dependencies] 743 | ptyprocess = ">=0.5" 744 | 745 | [[package]] 746 | name = "pkginfo" 747 | version = "1.9.6" 748 | description = "Query metadata from sdists / bdists / installed packages." 749 | optional = false 750 | python-versions = ">=3.6" 751 | files = [ 752 | {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, 753 | {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, 754 | ] 755 | 756 | [package.extras] 757 | testing = ["pytest", "pytest-cov"] 758 | 759 | [[package]] 760 | name = "platformdirs" 761 | version = "4.0.0" 762 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 763 | optional = false 764 | python-versions = ">=3.7" 765 | files = [ 766 | {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, 767 | {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, 768 | ] 769 | 770 | [package.extras] 771 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 772 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 773 | 774 | [[package]] 775 | name = "prompt-toolkit" 776 | version = "3.0.41" 777 | description = "Library for building powerful interactive command lines in Python" 778 | optional = false 779 | python-versions = ">=3.7.0" 780 | files = [ 781 | {file = "prompt_toolkit-3.0.41-py3-none-any.whl", hash = "sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"}, 782 | {file = "prompt_toolkit-3.0.41.tar.gz", hash = "sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0"}, 783 | ] 784 | 785 | [package.dependencies] 786 | wcwidth = "*" 787 | 788 | [[package]] 789 | name = "ptyprocess" 790 | version = "0.7.0" 791 | description = "Run a subprocess in a pseudo terminal" 792 | optional = false 793 | python-versions = "*" 794 | files = [ 795 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 796 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 797 | ] 798 | 799 | [[package]] 800 | name = "pure-eval" 801 | version = "0.2.2" 802 | description = "Safely evaluate AST nodes without side effects" 803 | optional = false 804 | python-versions = "*" 805 | files = [ 806 | {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, 807 | {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, 808 | ] 809 | 810 | [package.extras] 811 | tests = ["pytest"] 812 | 813 | [[package]] 814 | name = "pycodestyle" 815 | version = "2.11.1" 816 | description = "Python style guide checker" 817 | optional = false 818 | python-versions = ">=3.8" 819 | files = [ 820 | {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, 821 | {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, 822 | ] 823 | 824 | [[package]] 825 | name = "pycparser" 826 | version = "2.21" 827 | description = "C parser in Python" 828 | optional = false 829 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 830 | files = [ 831 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 832 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 833 | ] 834 | 835 | [[package]] 836 | name = "pyflakes" 837 | version = "3.1.0" 838 | description = "passive checker of Python programs" 839 | optional = false 840 | python-versions = ">=3.8" 841 | files = [ 842 | {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, 843 | {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, 844 | ] 845 | 846 | [[package]] 847 | name = "pygments" 848 | version = "2.17.2" 849 | description = "Pygments is a syntax highlighting package written in Python." 850 | optional = false 851 | python-versions = ">=3.7" 852 | files = [ 853 | {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, 854 | {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, 855 | ] 856 | 857 | [package.extras] 858 | plugins = ["importlib-metadata"] 859 | windows-terminal = ["colorama (>=0.4.6)"] 860 | 861 | [[package]] 862 | name = "pywin32-ctypes" 863 | version = "0.2.2" 864 | description = "A (partial) reimplementation of pywin32 using ctypes/cffi" 865 | optional = false 866 | python-versions = ">=3.6" 867 | files = [ 868 | {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, 869 | {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, 870 | ] 871 | 872 | [[package]] 873 | name = "readme-renderer" 874 | version = "42.0" 875 | description = "readme_renderer is a library for rendering readme descriptions for Warehouse" 876 | optional = false 877 | python-versions = ">=3.8" 878 | files = [ 879 | {file = "readme_renderer-42.0-py3-none-any.whl", hash = "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d"}, 880 | {file = "readme_renderer-42.0.tar.gz", hash = "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1"}, 881 | ] 882 | 883 | [package.dependencies] 884 | docutils = ">=0.13.1" 885 | nh3 = ">=0.2.14" 886 | Pygments = ">=2.5.1" 887 | 888 | [package.extras] 889 | md = ["cmarkgfm (>=0.8.0)"] 890 | 891 | [[package]] 892 | name = "requests" 893 | version = "2.32.0" 894 | description = "Python HTTP for Humans." 895 | optional = false 896 | python-versions = ">=3.8" 897 | files = [ 898 | {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, 899 | {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, 900 | ] 901 | 902 | [package.dependencies] 903 | certifi = ">=2017.4.17" 904 | charset-normalizer = ">=2,<4" 905 | idna = ">=2.5,<4" 906 | urllib3 = ">=1.21.1,<3" 907 | 908 | [package.extras] 909 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 910 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 911 | 912 | [[package]] 913 | name = "requests-toolbelt" 914 | version = "1.0.0" 915 | description = "A utility belt for advanced users of python-requests" 916 | optional = false 917 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 918 | files = [ 919 | {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, 920 | {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, 921 | ] 922 | 923 | [package.dependencies] 924 | requests = ">=2.0.1,<3.0.0" 925 | 926 | [[package]] 927 | name = "rfc3986" 928 | version = "2.0.0" 929 | description = "Validating URI References per RFC 3986" 930 | optional = false 931 | python-versions = ">=3.7" 932 | files = [ 933 | {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, 934 | {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, 935 | ] 936 | 937 | [package.extras] 938 | idna2008 = ["idna"] 939 | 940 | [[package]] 941 | name = "rich" 942 | version = "13.7.0" 943 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 944 | optional = false 945 | python-versions = ">=3.7.0" 946 | files = [ 947 | {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, 948 | {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, 949 | ] 950 | 951 | [package.dependencies] 952 | markdown-it-py = ">=2.2.0" 953 | pygments = ">=2.13.0,<3.0.0" 954 | 955 | [package.extras] 956 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 957 | 958 | [[package]] 959 | name = "secretstorage" 960 | version = "3.3.3" 961 | description = "Python bindings to FreeDesktop.org Secret Service API" 962 | optional = false 963 | python-versions = ">=3.6" 964 | files = [ 965 | {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, 966 | {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, 967 | ] 968 | 969 | [package.dependencies] 970 | cryptography = ">=2.0" 971 | jeepney = ">=0.6" 972 | 973 | [[package]] 974 | name = "six" 975 | version = "1.16.0" 976 | description = "Python 2 and 3 compatibility utilities" 977 | optional = false 978 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 979 | files = [ 980 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 981 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 982 | ] 983 | 984 | [[package]] 985 | name = "stack-data" 986 | version = "0.6.3" 987 | description = "Extract data from python stack frames and tracebacks for informative displays" 988 | optional = false 989 | python-versions = "*" 990 | files = [ 991 | {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, 992 | {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, 993 | ] 994 | 995 | [package.dependencies] 996 | asttokens = ">=2.1.0" 997 | executing = ">=1.2.0" 998 | pure-eval = "*" 999 | 1000 | [package.extras] 1001 | tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] 1002 | 1003 | [[package]] 1004 | name = "traitlets" 1005 | version = "5.13.0" 1006 | description = "Traitlets Python configuration system" 1007 | optional = false 1008 | python-versions = ">=3.8" 1009 | files = [ 1010 | {file = "traitlets-5.13.0-py3-none-any.whl", hash = "sha256:baf991e61542da48fe8aef8b779a9ea0aa38d8a54166ee250d5af5ecf4486619"}, 1011 | {file = "traitlets-5.13.0.tar.gz", hash = "sha256:9b232b9430c8f57288c1024b34a8f0251ddcc47268927367a0dd3eeaca40deb5"}, 1012 | ] 1013 | 1014 | [package.extras] 1015 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 1016 | test = ["argcomplete (>=3.0.3)", "mypy (>=1.6.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] 1017 | 1018 | [[package]] 1019 | name = "twine" 1020 | version = "4.0.2" 1021 | description = "Collection of utilities for publishing packages on PyPI" 1022 | optional = false 1023 | python-versions = ">=3.7" 1024 | files = [ 1025 | {file = "twine-4.0.2-py3-none-any.whl", hash = "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8"}, 1026 | {file = "twine-4.0.2.tar.gz", hash = "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8"}, 1027 | ] 1028 | 1029 | [package.dependencies] 1030 | importlib-metadata = ">=3.6" 1031 | keyring = ">=15.1" 1032 | pkginfo = ">=1.8.1" 1033 | readme-renderer = ">=35.0" 1034 | requests = ">=2.20" 1035 | requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" 1036 | rfc3986 = ">=1.4.0" 1037 | rich = ">=12.0.0" 1038 | urllib3 = ">=1.26.0" 1039 | 1040 | [[package]] 1041 | name = "types-setuptools" 1042 | version = "68.2.0.2" 1043 | description = "Typing stubs for setuptools" 1044 | optional = false 1045 | python-versions = ">=3.7" 1046 | files = [ 1047 | {file = "types-setuptools-68.2.0.2.tar.gz", hash = "sha256:09efc380ad5c7f78e30bca1546f706469568cf26084cfab73ecf83dea1d28446"}, 1048 | {file = "types_setuptools-68.2.0.2-py3-none-any.whl", hash = "sha256:d5b5ff568ea2474eb573dcb783def7dadfd9b1ff638bb653b3c7051ce5aeb6d1"}, 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "typing-extensions" 1053 | version = "4.8.0" 1054 | description = "Backported and Experimental Type Hints for Python 3.8+" 1055 | optional = false 1056 | python-versions = ">=3.8" 1057 | files = [ 1058 | {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, 1059 | {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "urllib3" 1064 | version = "2.2.2" 1065 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1066 | optional = false 1067 | python-versions = ">=3.8" 1068 | files = [ 1069 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 1070 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 1071 | ] 1072 | 1073 | [package.extras] 1074 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1075 | h2 = ["h2 (>=4,<5)"] 1076 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1077 | zstd = ["zstandard (>=0.18.0)"] 1078 | 1079 | [[package]] 1080 | name = "wcwidth" 1081 | version = "0.2.12" 1082 | description = "Measures the displayed width of unicode strings in a terminal" 1083 | optional = false 1084 | python-versions = "*" 1085 | files = [ 1086 | {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, 1087 | {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "wheel" 1092 | version = "0.42.0" 1093 | description = "A built-package format for Python" 1094 | optional = false 1095 | python-versions = ">=3.7" 1096 | files = [ 1097 | {file = "wheel-0.42.0-py3-none-any.whl", hash = "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d"}, 1098 | {file = "wheel-0.42.0.tar.gz", hash = "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8"}, 1099 | ] 1100 | 1101 | [package.extras] 1102 | test = ["pytest (>=6.0.0)", "setuptools (>=65)"] 1103 | 1104 | [[package]] 1105 | name = "zipp" 1106 | version = "3.19.1" 1107 | description = "Backport of pathlib-compatible object wrapper for zip files" 1108 | optional = false 1109 | python-versions = ">=3.8" 1110 | files = [ 1111 | {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, 1112 | {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, 1113 | ] 1114 | 1115 | [package.extras] 1116 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 1117 | test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 1118 | 1119 | [metadata] 1120 | lock-version = "2.0" 1121 | python-versions = "^3.11" 1122 | content-hash = "8605cd7f71e8245448c52d0128b1949633ed502dbaf8108cdb611d1d11bbfd75" 1123 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | ################### BLACK ################### 2 | [tool.black] 3 | line-length = 100 4 | 5 | 6 | ################### ISORT ################### 7 | [tool.isort] 8 | not_skip = "__init__.py" 9 | force_grid_wrap = 0 10 | include_trailing_comma = true 11 | line_length = 100 12 | multi_line_output = 3 13 | order_by_type = "1" 14 | 15 | known_first_party = ["todo"] 16 | known_third_party = [] 17 | 18 | sections = ["FUTURE", "STDLIB", "THIRDPARTY", "CORE", "FIRSTPARTY", "LOCALFOLDER"] 19 | 20 | 21 | ################### POETRY ################### 22 | [tool.poetry] 23 | name = "td-cli" 24 | version = "1.0.0" 25 | description = "A command line todo manager" 26 | 27 | authors = [ 28 | "Darri Steinn Konn Konradsson " 29 | ] 30 | 31 | readme = "README.md" 32 | 33 | repository = "https://github.com/darrikonn/td-cli" 34 | homepage = "https://github.com/darrikonn/td-cli" 35 | 36 | keywords = ["td-cli"] 37 | 38 | [tool.poetry.dependencies] 39 | python = "^3.11" 40 | 41 | [tool.poetry.group.dev.dependencies] 42 | mypy = "^1.7.1" 43 | black = "^24.3.0" 44 | isort = "^5.12.0" 45 | ipdb = "^0.13.13" 46 | ipython = "^8.17.2" 47 | flake8 = "^6.1.0" 48 | wheel = "^0.42.0" 49 | twine = "^4.0.2" 50 | types-setuptools = "^68.2.0.2" 51 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E128,E172,E203,E241,E712,E731,W503 3 | max-line-length = 120 4 | max-complexity = 10 5 | 6 | [metadata] 7 | license_file = LICENSE 8 | 9 | [bdist_wheel] 10 | universal=1 11 | 12 | [mypy] 13 | python_version = 3.8 14 | warn_return_any = False 15 | warn_unused_configs = True 16 | ignore_missing_imports = True 17 | follow_imports = silent 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from codecs import open 2 | from os import path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | # Get the long description from the README file 9 | with open(path.join(here, "README.md"), encoding="utf-8") as f: 10 | long_description = f.read() 11 | 12 | setup( 13 | name="td-cli", 14 | version="2.2.1", 15 | description="A todo command line manager", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/darrikonn/td-cli", 19 | author="Darri Steinn Konn Konradsson", 20 | author_email="darrikonn@gmail.com", 21 | license="MIT", 22 | classifiers=[ 23 | "Development Status :: 4 - Beta", 24 | "License :: OSI Approved :: MIT License", 25 | "Programming Language :: Python :: 3.6", 26 | "Programming Language :: Python :: 3.7", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | ], 33 | keywords="todo commandline td-cli", 34 | packages=find_packages(exclude=["contrib", "docs", "tests"]), 35 | python_requires=">=3.6", 36 | install_requires=[], 37 | entry_points={"console_scripts": ["td=todo:main"]}, 38 | project_urls={"Source": "https://github.com/darrikonn/td-cli"}, 39 | ) 40 | -------------------------------------------------------------------------------- /todo/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | 5 | if sys.version_info < (3,): 6 | sys.exit("Sorry, td-cli only supports Python >= 3") 7 | 8 | 9 | def main(args=None): 10 | from todo.commands import Commands 11 | from todo.exceptions import TodoException 12 | from todo.parser import Parser 13 | from todo.renderers import RenderError 14 | 15 | if args is None: 16 | args = sys.argv[1:] 17 | verbose = any(verbose in args for verbose in ("--verbose", "-v")) 18 | 19 | try: 20 | command, arguments = Parser().parseopts(args) 21 | Commands(command).run(arguments) 22 | except TodoException as exc: 23 | RenderError(exc.message, exc.details, verbose, exc.type).render() 24 | except Exception as exc: 25 | RenderError( 26 | "An unknown error occurred when running `{bold}{command}{reset}`", 27 | exc, 28 | verbose, 29 | "Exception", 30 | ).render(command=("td " + " ".join(args)).strip()) 31 | -------------------------------------------------------------------------------- /todo/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import sys 4 | 5 | import todo 6 | 7 | if __name__ == "__main__": 8 | sys.exit(todo.main()) 9 | -------------------------------------------------------------------------------- /todo/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from todo.commands import config, group, todo 2 | from todo.constants import COMMANDS 3 | from todo.services import Service 4 | 5 | 6 | class Commands: 7 | commands_dict = { 8 | COMMANDS.ADD_TODO: todo.Add, 9 | COMMANDS.COMPLETE_TODO: todo.Complete, 10 | COMMANDS.COUNT_TODOS: todo.Count, 11 | COMMANDS.DELETE_TODO: todo.Delete, 12 | COMMANDS.EDIT_TODO: todo.Edit, 13 | COMMANDS.GET_TODO: todo.Get, 14 | COMMANDS.LIST_TODOS: todo.List, 15 | COMMANDS.UNCOMPLETE_TODO: todo.Uncomplete, 16 | COMMANDS.INITIALIZE_CONFIG: config.Initialize, 17 | COMMANDS.ADD_GROUP: group.Add, 18 | COMMANDS.DELETE_GROUP: group.Delete, 19 | COMMANDS.GET_GROUP: group.Get, 20 | COMMANDS.LIST_GROUPS: group.List, 21 | COMMANDS.PRESET_GROUP: group.Preset, 22 | } 23 | 24 | def __init__(self, command): 25 | self.command = command 26 | 27 | def run(self, arguments): 28 | with Service() as service: 29 | command = self.commands_dict[self.command](service) 30 | command.run(arguments) 31 | -------------------------------------------------------------------------------- /todo/commands/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from todo.exceptions import TodoException 4 | 5 | 6 | class Command(ABC): 7 | def __init__(self, service): 8 | self.service = service 9 | 10 | def _get_todo_or_raise(self, id): 11 | group = self.service.group.get_active_group() 12 | todo = self.service.todo.get(id, group[0]) 13 | if todo is None: 14 | raise TodoException("{bold}{reset} not found" % id) 15 | 16 | return todo 17 | 18 | def _get_group_or_raise(self, name): 19 | group = self.service.group.get(name) 20 | if group is None and name != "global": 21 | raise TodoException(" not found".format(name=name)) 22 | 23 | return group 24 | 25 | @abstractmethod 26 | def run(self, *args, **kwargs): 27 | pass 28 | -------------------------------------------------------------------------------- /todo/commands/config/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | from .initialize import Initialize 3 | -------------------------------------------------------------------------------- /todo/commands/config/initialize.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from todo.commands.base import Command 4 | from todo.renderers import RenderInput, RenderOutput 5 | from todo.settings import EXAMPLE_CONFIG 6 | 7 | 8 | class Initialize(Command): 9 | def run(self, args): 10 | cwd = Path.expanduser(Path.cwd()) 11 | location = Path( 12 | RenderInput("[?] Configuration location? [{cwd}] ").render(cwd=cwd) or cwd 13 | ).expanduser() 14 | if not location.is_dir(): 15 | RenderOutput("Directory {red}{location}{reset} does not exist").render( 16 | location=location 17 | ) 18 | return RenderOutput("Abort!").render() 19 | file = Path(f"{location}/.td.cfg") 20 | if file.exists(): 21 | RenderOutput("Configuration file {red}{file}{reset} already exists").render(file=file) 22 | return RenderOutput("Abort!").render() 23 | 24 | group = RenderInput("[?] Choose your default group? ").render() 25 | if self.service.group.get(group) is None: 26 | RenderOutput("Group {red}{group}{reset} does not exist").render(group=group or '""') 27 | return RenderOutput("Abort!").render() 28 | 29 | with open(file, "w+") as f: 30 | f.write(EXAMPLE_CONFIG.format(group=group)) 31 | 32 | RenderOutput("\nConfiguration file {green}successfully{reset} created!").render() 33 | -------------------------------------------------------------------------------- /todo/commands/group/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | from .add import Add 3 | from .delete import Delete 4 | from .get import Get 5 | from .list import List 6 | from .preset import Preset 7 | -------------------------------------------------------------------------------- /todo/commands/group/add.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Error, IntegrityError 2 | 3 | from todo.commands.base import Command 4 | from todo.exceptions import TodoException 5 | from todo.renderers import RenderOutput 6 | 7 | 8 | class Add(Command): 9 | def run(self, args): 10 | try: 11 | group_name = self.service.group.add(args.name) 12 | 13 | RenderOutput("Created group {blue}{group_name}").render(group_name=group_name) 14 | except IntegrityError as e: 15 | raise TodoException("`{bold}{reset}` already exists." % args.name, e) 16 | except Error as e: 17 | raise TodoException("Error occurred, could not create a new group", e) 18 | -------------------------------------------------------------------------------- /todo/commands/group/delete.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Error 2 | 3 | from todo.commands.base import Command 4 | from todo.exceptions import TodoException 5 | from todo.renderers import RenderInput, RenderOutput 6 | from todo.utils import singular_or_plural 7 | 8 | 9 | class Delete(Command): 10 | def run(self, args): 11 | try: 12 | group = self._get_group_or_raise(args.name) 13 | if group[0] is None or group[0] == "global": 14 | raise TodoException( 15 | "Can't delete `{bold}{reset}`. It must always exist" 16 | ) 17 | if not args.skip_prompt: 18 | todo_count = group[2] + group[1] 19 | post_text = "" 20 | if todo_count > 0: 21 | RenderOutput( 22 | "By deleting group {blue}{group_name}{reset}, " 23 | "you'll also delete {bold}{todo_count}{normal} todo{singular_or_plural} in that group" 24 | ).render( 25 | group_name=args.name, 26 | todo_count=todo_count, 27 | singular_or_plural=singular_or_plural(todo_count), 28 | ) 29 | post_text = ", and {todo_count} todo{singular_or_plural}" 30 | choice = RenderInput( 31 | "[?] Are you sure you want to delete group {blue}{group_name}{reset}? [Y|n] " 32 | ).render(group_name=group[0]) 33 | if choice not in ("y", "yes", ""): 34 | return RenderOutput("Abort!").render() 35 | self.service.group.delete(group[0]) 36 | 37 | RenderOutput("{red}Deleted{reset} {bold}{group_name}{normal}" + post_text).render( 38 | group_name=group[0], 39 | todos=post_text, 40 | singular_or_plural=singular_or_plural(todo_count), 41 | todo_count=todo_count, 42 | ) 43 | except Error as e: 44 | raise TodoException( 45 | "Error occurred, could not delete `{bold}{reset}`" % args.name, e 46 | ) 47 | -------------------------------------------------------------------------------- /todo/commands/group/get.py: -------------------------------------------------------------------------------- 1 | from todo.commands.base import Command 2 | from todo.commands.todo import List as ListTodos 3 | 4 | 5 | class Get(Command): 6 | def run(self, args): 7 | setattr(args, "group", args.name) 8 | ListTodos(self.service).run(args) 9 | -------------------------------------------------------------------------------- /todo/commands/group/list.py: -------------------------------------------------------------------------------- 1 | from todo.commands.base import Command 2 | from todo.renderers import RenderOutput 3 | from todo.utils import interpret_state, singular_or_plural 4 | 5 | 6 | class List(Command): 7 | def run(self, args): 8 | groups = self.service.group.get_all(args.state) 9 | if not groups: 10 | return RenderOutput("No{state} {bold}{blue}groups{reset} exist").render( 11 | state=interpret_state(args.state) 12 | ) 13 | 14 | for group in groups: 15 | RenderOutput( 16 | "{bold}{blue}{group_name}{reset}: {items} item{singular_or_plural}: " 17 | "{completed} completed, {uncompleted} left" 18 | ).render( 19 | group_name=group[0], 20 | items=group[1], 21 | singular_or_plural=singular_or_plural(group[1]), 22 | uncompleted=group[2], 23 | completed=group[3], 24 | ) 25 | 26 | self._print_footer(groups) 27 | 28 | def _print_footer(self, groups): 29 | group_count = len(groups) 30 | summary = [res for res in zip(*groups)][2:] 31 | completed_groups_count = sum(1 for x in summary[0] if x == 0) 32 | 33 | RenderOutput( 34 | "\n{grey}{group_count} group{singular_or_plural}: {completed} completed, {uncompleted} left" 35 | ).render( 36 | group_count=group_count, 37 | singular_or_plural=singular_or_plural(group_count), 38 | completed=completed_groups_count, 39 | uncompleted=group_count - completed_groups_count, 40 | ) 41 | -------------------------------------------------------------------------------- /todo/commands/group/preset.py: -------------------------------------------------------------------------------- 1 | from todo.commands.base import Command 2 | from todo.renderers import RenderOutput 3 | 4 | 5 | class Preset(Command): 6 | def run(self, args): 7 | group = self._get_group_or_raise(args.name) 8 | 9 | self.service.group.use(group[0]) 10 | 11 | RenderOutput("Set {blue}{group_name}{reset} as default").render( 12 | group_name=group[0] or "global" 13 | ) 14 | -------------------------------------------------------------------------------- /todo/commands/todo/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | from .add import Add 3 | from .complete import Complete 4 | from .count import Count 5 | from .delete import Delete 6 | from .edit import Edit 7 | from .get import Get 8 | from .list import List 9 | from .list_interactive import ListInteractive 10 | from .uncomplete import Uncomplete 11 | -------------------------------------------------------------------------------- /todo/commands/todo/add.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Error 2 | 3 | from todo.commands.base import Command 4 | from todo.exceptions import TodoException 5 | from todo.renderers import RenderOutput 6 | from todo.settings import config 7 | from todo.utils import get_user_input 8 | 9 | 10 | class Add(Command): 11 | def run(self, args): 12 | try: 13 | if args.edit: 14 | details = get_user_input(config["editor"]) 15 | else: 16 | details = args.details or args.name 17 | 18 | if args.group is None: 19 | group = self.service.group.get_active_group() 20 | else: 21 | group = self.service.group.get(args.group) 22 | todo_id = self.service.todo.add(args.name, details, group[0], completed=args.state) 23 | 24 | RenderOutput("Created todo {bold}{todo_id}").render(todo_id=todo_id) 25 | except Error as e: 26 | raise TodoException("Error occurred, could not create a new todo", e) 27 | -------------------------------------------------------------------------------- /todo/commands/todo/complete.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Error 2 | 3 | from todo.commands.base import Command 4 | from todo.exceptions import TodoException 5 | from todo.renderers import RenderOutput 6 | 7 | 8 | class Complete(Command): 9 | def run(self, args): 10 | try: 11 | todo = self._get_todo_or_raise(args.id) 12 | self.service.todo.complete(todo[0]) 13 | 14 | RenderOutput("{bold}{green}✓ {reset}{bold}{todo_id}{reset}: {name}").render( 15 | todo_id=todo[0], name=todo[2] 16 | ) 17 | except Error as e: 18 | raise TodoException("Error occurred, could not complete " % args.id, e) 19 | -------------------------------------------------------------------------------- /todo/commands/todo/count.py: -------------------------------------------------------------------------------- 1 | from todo.commands.base import Command 2 | from todo.exceptions import TodoException 3 | from todo.renderers import RenderOutput 4 | 5 | 6 | class Count(Command): 7 | def run(self, args): 8 | if args.group is None: 9 | group = self.service.group.get_active_group() 10 | else: 11 | group = self.service.group.get(args.group) 12 | 13 | if group is None: 14 | raise TodoException(" not found".format(name=args.group)) 15 | 16 | todos = self.service.todo.get_all(group[0], args.state) 17 | 18 | RenderOutput("{count}").render(count=len(todos)) 19 | -------------------------------------------------------------------------------- /todo/commands/todo/delete.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Error 2 | 3 | from todo.commands.base import Command 4 | from todo.exceptions import TodoException 5 | from todo.renderers import RenderInput, RenderOutput, RenderOutputWithTextwrap 6 | 7 | 8 | class Delete(Command): 9 | def run(self, args): 10 | try: 11 | todo = self._get_todo_or_raise(args.id) 12 | if not args.skip_prompt: 13 | choice = RenderInput( 14 | "[?] Are you sure you want to delete todo {bold}{todo_id}{normal}? [Y|n] " 15 | ).render(todo_id=todo[0]) 16 | if choice not in ("", "y", "ye", "yes"): 17 | return RenderOutput("Abort!").render() 18 | self.service.todo.delete(todo[0]) 19 | 20 | RenderOutputWithTextwrap( 21 | "{red}Deleted{reset} {bold}{todo_id}{reset}: ", "{name}" 22 | ).render(name=todo[2], todo_id=todo[0], subsequent_indent=" " * 16) 23 | except Error as e: 24 | raise TodoException("Error occurred, could not delete " % args.id, e) 25 | -------------------------------------------------------------------------------- /todo/commands/todo/edit.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Error 2 | 3 | from todo.commands.base import Command 4 | from todo.exceptions import TodoException 5 | from todo.renderers import RenderOutput 6 | from todo.settings import config 7 | from todo.utils import get_user_input 8 | 9 | 10 | class Edit(Command): 11 | def run(self, args): 12 | try: 13 | todo = self._get_todo_or_raise(args.id) 14 | if not (args.name or args.details or args.group): 15 | details = get_user_input(config["editor"], str.encode(todo[3])) 16 | self.service.todo.edit_details(todo[0], details) 17 | else: 18 | if args.group: 19 | group = self._get_group_or_raise(args.group) 20 | self.service.todo.set_group(todo[0], group[0]) 21 | if args.name: 22 | self.service.todo.edit_name(todo[0], args.name) 23 | if args.details: 24 | self.service.todo.edit_details(todo[0], args.details) 25 | 26 | RenderOutput("Edited {bold}{todo_id}{reset}: {name}").render( 27 | todo_id=todo[0], name=args.name or todo[2] 28 | ) 29 | except Error as e: 30 | raise TodoException("Error occurred, could not edit " % args.id, e) 31 | -------------------------------------------------------------------------------- /todo/commands/todo/get.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Error 2 | 3 | from todo.commands.base import Command 4 | from todo.exceptions import TodoException 5 | from todo.renderers import RenderOutput, RenderOutputWithTextwrap 6 | 7 | 8 | class Get(Command): 9 | def run(self, args): 10 | try: 11 | todo = self._get_todo_or_raise(args.id) 12 | RenderOutput("{subsequent_indent}{bold}{blue}{group_name}{reset}\n").render( 13 | group_name=todo[1] or "UNGROUPED", subsequent_indent=" " * 4 14 | ) 15 | 16 | RenderOutput("{details}").render(details=todo[3]) 17 | 18 | RenderOutputWithTextwrap( 19 | "\n{grey}{completed} {bold}{todo_id}{normal}: ", "{details}" 20 | ).render(details=todo[2], completed="✓" if todo[4] else "x", todo_id=todo[0]) 21 | except Error as e: 22 | raise TodoException( 23 | "Error occurred, could not get {bold}{reset}" % args.id, e 24 | ) 25 | -------------------------------------------------------------------------------- /todo/commands/todo/list.py: -------------------------------------------------------------------------------- 1 | from todo.commands.base import Command 2 | from todo.exceptions import TodoException 3 | from todo.renderers import RenderOutput, RenderOutputWithTextwrap 4 | from todo.utils import singular_or_plural 5 | 6 | from .list_interactive import ListInteractive 7 | 8 | 9 | class List(Command): 10 | def run(self, args): 11 | if args.interactive: 12 | return ListInteractive(self.service).run(args) 13 | 14 | if args.group is None: 15 | group = self.service.group.get_active_group() 16 | else: 17 | group = self.service.group.get(args.group) 18 | 19 | if group is None: 20 | raise TodoException(" not found".format(name=args.group)) 21 | 22 | todos = self.service.todo.get_all(group[0], args.state) 23 | 24 | if not args.raw: 25 | RenderOutput("{subsequent_indent}{bold}{blue}{group_name}{reset}\n").render( 26 | subsequent_indent=" " * 4, group_name=group[0] or "global" 27 | ) 28 | 29 | for todo in todos: 30 | RenderOutputWithTextwrap("{completed} {bold}{todo_id}{reset}: ", "{name}").render( 31 | completed="✓" if todo[3] else "x", name=todo[1], todo_id=todo[0] 32 | ) 33 | 34 | if not args.raw: 35 | RenderOutput( 36 | "{prefix}{grey}{items} item{singular_or_plural}: {completed} completed, {uncompleted} left" 37 | ).render( 38 | prefix="\n" if group[1] > 0 else "", 39 | items=group[1], 40 | singular_or_plural=singular_or_plural(group[1]), 41 | uncompleted=group[2], 42 | completed=group[3], 43 | ) 44 | -------------------------------------------------------------------------------- /todo/commands/todo/list_interactive.py: -------------------------------------------------------------------------------- 1 | from todo.commands.base import Command 2 | from todo.constants import COMMAND_MODES 3 | from todo.constants import INTERACTIVE_COMMANDS as COMMANDS 4 | from todo.exceptions import TodoException 5 | from todo.utils import singular_or_plural 6 | from todo.utils.menu.vertical_tracker import VerticalTracker 7 | 8 | 9 | class ListInteractive(Command): 10 | DELETE_MODE_COMMANDS = ( 11 | COMMANDS.ADD, 12 | COMMANDS.QUIT, 13 | COMMANDS.RECOVER, 14 | COMMANDS.UP, 15 | COMMANDS.DOWN, 16 | ) 17 | EMPTY_MODE_COMMANDS = (COMMANDS.ADD, COMMANDS.QUIT) 18 | DEFAULT_MODE_COMMANDS = ( 19 | COMMANDS.ADD, 20 | COMMANDS.QUIT, 21 | COMMANDS.DELETE, 22 | COMMANDS.EDIT, 23 | COMMANDS.TOGGLE, 24 | COMMANDS.UP, 25 | COMMANDS.DOWN, 26 | ) 27 | # ADD_MODE_COMMANDS is a special case outside of this scope 28 | # EDIT_MODE_COMMANDS is a special case outside of this scope 29 | 30 | def run(self, args): # noqa: C901 31 | try: 32 | from todo.utils.menu import Menu 33 | except ImportError as e: 34 | raise TodoException("Sorry! The interactive mode is not supported by your system", e) 35 | 36 | if args.group is None: 37 | group = self.service.group.get_active_group() 38 | else: 39 | group = self.service.group.get(args.group) 40 | 41 | if group is None: 42 | raise TodoException(" not found".format(name=args.group)) 43 | 44 | todos = self.service.todo.get_all(group[0], args.state) 45 | 46 | with Menu() as menu: 47 | menu.clear() 48 | 49 | tracker = VerticalTracker(todos, group) 50 | while True: 51 | menu.refresh() 52 | menu.render_header("{group_name}".format(group_name=tracker.group.name or "global")) 53 | menu.render_subheader( 54 | "{items} item{singular_or_plural}: {completed} completed, {uncompleted} left".format( 55 | items=tracker.group.items, 56 | singular_or_plural=singular_or_plural(tracker.group.items), 57 | completed=tracker.group.completed, 58 | uncompleted=tracker.group.uncompleted, 59 | ) 60 | ) 61 | 62 | self._render_todos(menu, tracker) 63 | 64 | mode = self._get_mode(tracker) 65 | if mode == COMMAND_MODES.EMPTY: 66 | menu.render_commands(tracker.commands_offset, mode=COMMAND_MODES.EMPTY) 67 | elif mode == COMMAND_MODES.DELETE: 68 | menu.render_commands(tracker.commands_offset, mode=COMMAND_MODES.DELETE) 69 | else: 70 | menu.render_commands(tracker.commands_offset) 71 | 72 | command = self._interpret_command(menu.get_command(), mode) 73 | 74 | if command == COMMANDS.DOWN: 75 | tracker.move_down() 76 | elif command == COMMANDS.UP: 77 | tracker.move_up() 78 | elif command == COMMANDS.RECOVER: 79 | tracker.recover() 80 | elif command == COMMANDS.TOGGLE: 81 | tracker.toggle(self.service.todo) 82 | elif command == COMMANDS.ADD: 83 | # add empty line 84 | tracker.add(("??????", "", "", None)) 85 | 86 | # rerender todos 87 | self._render_todos(menu, tracker) 88 | 89 | # render add commands 90 | menu.render_commands(tracker.commands_offset, mode=COMMAND_MODES.ADD) 91 | 92 | new_todo_name = menu.edit_text("", tracker.index) 93 | if new_todo_name is not None: 94 | tracker.update(new_todo_name, self.service.todo) 95 | else: 96 | tracker.remove() 97 | menu.clear() 98 | elif command == COMMANDS.EDIT: 99 | menu.render_commands(tracker.commands_offset, mode=COMMAND_MODES.EDIT) 100 | new_todo_name = menu.edit_text(tracker.current_todo.name, tracker.index) 101 | tracker.edit(new_todo_name, self.service.todo) 102 | elif command == COMMANDS.DELETE: 103 | tracker.mark_deleted() 104 | elif command == COMMANDS.QUIT: 105 | break 106 | 107 | tracker.delete_todos(self.service.todo) 108 | 109 | def _get_mode(self, tracker): 110 | if tracker.todos_count == 0: 111 | return COMMAND_MODES.EMPTY 112 | elif tracker.is_deleted(tracker.current_todo.id): 113 | return COMMAND_MODES.DELETE 114 | return COMMAND_MODES.DEFAULT 115 | 116 | def _interpret_command(self, command, mode): 117 | if mode == COMMAND_MODES.DELETE and command in self.DELETE_MODE_COMMANDS: 118 | return command 119 | elif mode == COMMAND_MODES.EMPTY and command in self.EMPTY_MODE_COMMANDS: 120 | return command 121 | elif mode == COMMAND_MODES.DEFAULT and command in self.DEFAULT_MODE_COMMANDS: 122 | return command 123 | return None 124 | 125 | def _render_todos(self, menu, tracker): 126 | for index, todo in enumerate(tracker.todos): 127 | menu.render_todo(todo, index, tracker.index, tracker.is_deleted(todo[0])) 128 | -------------------------------------------------------------------------------- /todo/commands/todo/uncomplete.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Error 2 | 3 | from todo.commands.base import Command 4 | from todo.exceptions import TodoException 5 | from todo.renderers import RenderOutput 6 | 7 | 8 | class Uncomplete(Command): 9 | def run(self, args): 10 | try: 11 | todo = self._get_todo_or_raise(args.id) 12 | self.service.todo.uncomplete(todo[0]) 13 | RenderOutput("{bold}{red}x {reset}{todo_id}{normal}: {name}").render( 14 | todo_id=todo[0], name=todo[2] 15 | ) 16 | except Error as e: 17 | raise TodoException("Error occurred, could not uncomplete " % args.id, e) 18 | -------------------------------------------------------------------------------- /todo/constants.py: -------------------------------------------------------------------------------- 1 | class COMMANDS: 2 | ADD_TODO = "add_todo" 3 | COMPLETE_TODO = "complete_todo" 4 | COUNT_TODOS = "count_todos" 5 | DELETE_TODO = "delete_todo" 6 | EDIT_TODO = "edit_todo" 7 | GET_TODO = "get_todo" 8 | LIST_TODOS = "list_todos" 9 | NAME_TODO = "name_todo" 10 | UNCOMPLETE_TODO = "uncomplete_todo" 11 | ADD_GROUP = "add_group" 12 | DELETE_GROUP = "delete_group" 13 | GET_GROUP = "get_group" 14 | LIST_GROUPS = "list_groups" 15 | PRESET_GROUP = "preset_group" 16 | INITIALIZE_CONFIG = "initialize_config" 17 | 18 | 19 | class INTERACTIVE_COMMANDS: 20 | ADD = "add" 21 | DELETE = "delete" 22 | DOWN = "down" 23 | EDIT = "edit" 24 | ENTER = "enter" 25 | ESCAPE = "escape" 26 | QUIT = "quit" 27 | RECOVER = "recover" 28 | TOGGLE = "toggle" 29 | UP = "up" 30 | 31 | 32 | class STATES: 33 | COMPLETED = "completed" 34 | UNCOMPLETED = "uncompleted" 35 | 36 | 37 | class COMMAND_MODES: 38 | ADD = "add" 39 | EMPTY = "empty" 40 | DEFAULT = "default" 41 | DELETE = "delete" 42 | EDIT = "edit" 43 | -------------------------------------------------------------------------------- /todo/exceptions.py: -------------------------------------------------------------------------------- 1 | class TodoException(Exception): 2 | def __init__(self, message, details=None, type="Error"): 3 | self.message = message 4 | self.details = details or message 5 | self.type = type 6 | 7 | def __str__(self): 8 | return self.message 9 | 10 | def __repr__(self): 11 | return self.message 12 | -------------------------------------------------------------------------------- /todo/parser/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from todo.exceptions import TodoException 4 | from todo.parser.subparsers import ( 5 | AddGroupParser, 6 | AddTodoParser, 7 | CountTodosParser, 8 | GroupParser, 9 | InitializeConfigParser, 10 | ListGroupsParser, 11 | ListTodosParser, 12 | TodoParser, 13 | ) 14 | from todo.renderers import RenderHelp 15 | from todo.utils import docstring, get_version 16 | 17 | 18 | @docstring(get_version()) 19 | class Parser: 20 | """ 21 | usage: td {add,add_group,[id],group,list,list_groups} ... 22 | 23 | td-cli %s 24 | Darri Steinn Konn Konradsson 25 | https://github.com/darrikonn/td-cli 26 | 27 | td-cli (td) is a command line todo manager, 28 | where you can organize and manage your todos across multiple projects. 29 | 30 | positional arguments: 31 | {...} commands 32 | add (a) add todo 33 | add-group (ag) add group 34 | count (c) count todos 35 | [id] manage todo 36 | group (g) manage group 37 | init-config (ic) initialize config 38 | list (ls, l) list todos *DEFAULT* 39 | list-groups (lg, lsg) list groups 40 | 41 | optional arguments: 42 | -h, --help show this help message and exit 43 | --completed, -c filter by completed todos 44 | --uncompleted, -u filter by uncompleted todos 45 | --raw, -r only show todos 46 | --group GROUP, -g GROUP 47 | filter by name of group 48 | --interactive, -i toggle interactive mode 49 | 50 | `td` defaults to `td list` 51 | """ 52 | 53 | def __init__(self): 54 | self.parser = argparse.ArgumentParser() 55 | self.parser.print_help = self._print_help 56 | self.parser.add_argument("command", nargs="?") 57 | 58 | _subparsers = { 59 | # add todo 60 | "a": AddTodoParser, 61 | "add": AddTodoParser, 62 | # add group 63 | "ag": AddGroupParser, 64 | "add-group": AddGroupParser, 65 | # count todos 66 | "c": CountTodosParser, 67 | "count": CountTodosParser, 68 | # get group 69 | "g": GroupParser, 70 | "group": GroupParser, 71 | # initialize config 72 | "ic": InitializeConfigParser, 73 | "init-config": InitializeConfigParser, 74 | # list groups 75 | "lg": ListGroupsParser, 76 | "lsg": ListGroupsParser, 77 | "list-groups": ListGroupsParser, 78 | # list todos 79 | "l": ListTodosParser, 80 | "ls": ListTodosParser, 81 | "list": ListTodosParser, 82 | } 83 | 84 | def _print_help(self): 85 | RenderHelp(self.__doc__).render() 86 | 87 | def _get_parser(self, args): 88 | command = self.parser.parse_known_args(args[:1])[0].command 89 | if command is None: 90 | return ListTodosParser() 91 | if command.isdigit() and len(command) <= 6: 92 | return TodoParser() 93 | 94 | parser = self._subparsers.get(command) 95 | if parser is None: 96 | if command == "list_groups": 97 | raise TodoException( 98 | "`{bold}list_groups{reset}` is deprecated, use `{bold}list-groups{reset} instead", 99 | type="DeprecatedException", 100 | ) 101 | if command == "add_group": 102 | raise TodoException( 103 | "`{bold}add_group{reset}` is deprecated, use `{bold}add-group{reset} instead", 104 | type="DeprecatedException", 105 | ) 106 | 107 | raise TodoException( 108 | "Unknown command `{bold}td %s{reset}`" % " ".join(args), type="UsageError" 109 | ) 110 | 111 | return parser(command) 112 | 113 | def parseopts(self, args): 114 | parser = self._get_parser(args) 115 | return parser.parseopts(args) 116 | -------------------------------------------------------------------------------- /todo/parser/base.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from abc import ABCMeta, abstractproperty 3 | 4 | from todo.renderers import RenderHelp 5 | from todo.utils import get_version 6 | 7 | 8 | def set_value(value): 9 | class Action(argparse.Action): 10 | def __call__(self, parser, args, values, option_string=None): 11 | setattr(args, self.dest, value) 12 | 13 | return Action 14 | 15 | 16 | def set_default_subparser(self, name, args, positional_args): 17 | for arg in args: 18 | if arg in ["-h", "--help"]: 19 | break 20 | else: 21 | for x in self._subparsers._actions: 22 | if not isinstance(x, argparse._SubParsersAction): 23 | continue 24 | for sp_name in x._name_parser_map.keys(): 25 | if sp_name in args: 26 | return 27 | if len(args) < positional_args: 28 | return args 29 | return args.insert(positional_args, name) 30 | 31 | 32 | setattr(argparse.ArgumentParser, "set_default_subparser", set_default_subparser) 33 | 34 | 35 | class BaseParser: 36 | __metaclass__ = ABCMeta 37 | 38 | @abstractproperty 39 | def command(self): 40 | raise NotImplementedError 41 | 42 | def __init__(self, command=None): 43 | self.parent = argparse.ArgumentParser(add_help=False) 44 | self.parent.add_argument( 45 | "--verbose", "-v", dest="verbose", action="store_true", help=argparse.SUPPRESS 46 | ) 47 | self.parent.add_argument( 48 | "--version", 49 | action="version", 50 | help=argparse.SUPPRESS, 51 | version="td version {version} - (C) Darri Steinn Konn Konradsson".format( 52 | version=get_version() 53 | ), 54 | ) 55 | 56 | self.root_parser = argparse.ArgumentParser(parents=[self.parent]) 57 | if command is None: 58 | self.parser = self.root_parser 59 | else: 60 | self.parser = self.root_parser.add_subparsers().add_parser( 61 | command, parents=[self.parent] 62 | ) 63 | self.parser.print_help = self.print_help 64 | 65 | def _add_parser(self, parent, *args, **kwargs): 66 | parser = parent.add_parser(parents=[self.parent], *args, **kwargs) 67 | if "help" not in kwargs: 68 | parser.print_help = self.print_help 69 | return parser 70 | 71 | def _add_arguments(self): 72 | pass 73 | 74 | def _set_defaults(self, args): 75 | pass 76 | 77 | def print_help(self): 78 | RenderHelp(self.__doc__).render() 79 | 80 | def parseopts(self, args): 81 | self._add_arguments() 82 | self._set_defaults(args) 83 | parsed_args = self.root_parser.parse_args(args) 84 | return (getattr(parsed_args, "command", None) or self.command, parsed_args) 85 | -------------------------------------------------------------------------------- /todo/parser/subparsers/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | from .add_group import AddGroupParser 3 | from .add_todo import AddTodoParser 4 | from .count_todos import CountTodosParser 5 | from .group import GroupParser 6 | from .initialize_config import InitializeConfigParser 7 | from .list_groups import ListGroupsParser 8 | from .list_todos import ListTodosParser 9 | from .todo import TodoParser 10 | -------------------------------------------------------------------------------- /todo/parser/subparsers/add_group.py: -------------------------------------------------------------------------------- 1 | from todo.constants import COMMANDS 2 | from todo.parser.base import BaseParser 3 | 4 | 5 | class AddGroupParser(BaseParser): 6 | """ 7 | usage: td add-group [name] 8 | td ag [name] 9 | 10 | add group 11 | 12 | positional arguments: 13 | name the new group's name 14 | 15 | optional arguments: 16 | -h, --help show this help message and exit 17 | """ 18 | 19 | command = COMMANDS.ADD_GROUP 20 | 21 | def _add_arguments(self): 22 | self.parser.add_argument("name", action="store", help="the new group's name") 23 | -------------------------------------------------------------------------------- /todo/parser/subparsers/add_todo.py: -------------------------------------------------------------------------------- 1 | from todo.constants import COMMANDS 2 | from todo.parser.base import BaseParser 3 | 4 | 5 | class AddTodoParser(BaseParser): 6 | """ 7 | usage: td add [name] [--complete] [--uncomplete] [--group GROUP] [--edit | --details DETAILS] 8 | td a [name] [-c] [-u] [-g GROUP] [-e | -d DETAILS] 9 | 10 | add todo 11 | 12 | positional arguments: 13 | name the new todo's name 14 | 15 | optional arguments: 16 | -h, --help show this help message and exit 17 | --complete, -c complete todo 18 | --uncomplete, -u uncomplete todo 19 | --group GROUP, -g GROUP 20 | name of todo's group 21 | --edit, -e edit the todo's details in your editor 22 | --details DETAILS, -d DETAILS 23 | the todo's details 24 | 25 | The editor defaults to `vi`, but you can choose your preferred one be setting: 26 | ``` 27 | [settings] 28 | editor: 29 | ``` 30 | in ~/.td.cfg 31 | """ 32 | 33 | command = COMMANDS.ADD_TODO 34 | 35 | def _add_arguments(self): 36 | self.parser.add_argument("name", action="store", help="the new todo's name") 37 | self.parser.add_argument( 38 | "--complete", "-c", dest="state", action="store_true", help="complete todo" 39 | ) 40 | self.parser.add_argument( 41 | "--uncomplete", "-u", dest="state", action="store_false", help="uncomplete todo" 42 | ) 43 | self.parser.add_argument("--group", "-g", action="store", help="name of todo's group") 44 | 45 | exclusive_parser = self.parser.add_mutually_exclusive_group() 46 | exclusive_parser.add_argument( 47 | "--edit", "-e", action="store_true", help="edit the todo's details in your editor" 48 | ) 49 | exclusive_parser.add_argument("--details", "-d", action="store", help="the todo's details") 50 | -------------------------------------------------------------------------------- /todo/parser/subparsers/count_todos.py: -------------------------------------------------------------------------------- 1 | from todo.constants import COMMANDS 2 | from todo.parser.base import BaseParser, set_value 3 | 4 | 5 | class CountTodosParser(BaseParser): 6 | """ 7 | usage: td count [--completed] [--uncompleted] [--group GROUP] 8 | td c [-c] [-u] [-g GROUP] 9 | 10 | count todos 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | --completed, -c filter by completed todos 15 | --uncompleted, -u filter by uncompleted todos 16 | --group GROUP, -g GROUP 17 | filter by name of group 18 | """ 19 | 20 | command = COMMANDS.COUNT_TODOS 21 | 22 | def _add_arguments(self): 23 | self.parser.add_argument( 24 | "--completed", 25 | "-c", 26 | dest="state", 27 | nargs=0, 28 | action=set_value(True), 29 | help="filter by completed todos", 30 | ) 31 | self.parser.add_argument( 32 | "--uncompleted", 33 | "-u", 34 | dest="state", 35 | nargs=0, 36 | action=set_value(False), 37 | help="filter by uncompleted todos", 38 | ) 39 | self.parser.add_argument("--group", "-g", action="store", help="filter by name of group") 40 | self.parser.usage = "td [--completed] [--uncompleted] [--group GROUP]" 41 | -------------------------------------------------------------------------------- /todo/parser/subparsers/group.py: -------------------------------------------------------------------------------- 1 | from todo.constants import COMMANDS 2 | from todo.parser.base import BaseParser, set_value 3 | 4 | 5 | class GroupParser(BaseParser): 6 | """ 7 | usage: td group [name] {list,delete,preset} ... 8 | td g [name] {l,d,p} ... 9 | 10 | manage group 11 | 12 | positional arguments: 13 | name name of the group 14 | {...} commands 15 | list (ls, l) list group's todos 16 | delete (d) delete group and its todos 17 | preset (p) set group as the default group when listing todos 18 | 19 | optional arguments: 20 | -h, --help show this help message and exit 21 | 22 | `td group [name]` defaults to `td group [name] ls` 23 | """ 24 | 25 | command = COMMANDS.GET_GROUP 26 | 27 | def _set_defaults(self, args): 28 | self.parser.set_default_subparser("list", args, 2) 29 | 30 | def _add_arguments(self): 31 | self.parser.add_argument("name", action="store", help="name of the group") 32 | subparser = self.parser.add_subparsers(dest="command", help="commands") 33 | 34 | delete_parser = self._add_parser( 35 | subparser, "delete", aliases=["d"], help="delete group and its todos" 36 | ) 37 | delete_parser.add_argument( 38 | "--yes", 39 | "-y", 40 | dest="skip_prompt", 41 | action="store_true", 42 | help="skip yes/no prompt when deleting group", 43 | ) 44 | delete_parser.set_defaults(command=COMMANDS.DELETE_GROUP) 45 | delete_parser.usage = "td group [name] delete [--yes]\n td group [name] d [-y]" 46 | delete_parser.description = "delete group and its todos" 47 | 48 | preset_parser = self._add_parser( 49 | subparser, 50 | "preset", 51 | aliases=["p"], 52 | help="set group as the default group when listing todos", 53 | ) 54 | preset_parser.set_defaults(command=COMMANDS.PRESET_GROUP) 55 | preset_parser.usage = "td group [name] preset\n td group [name] p" 56 | preset_parser.description = "set group as the default group when listing todos" 57 | 58 | list_parser = self._add_parser( 59 | subparser, "list", aliases=["ls", "l"], help="list group's todos" 60 | ) 61 | list_parser.add_argument( 62 | "--completed", 63 | "-c", 64 | dest="state", 65 | nargs=0, 66 | action=set_value(True), 67 | help="filter by completed todos", 68 | ) 69 | list_parser.add_argument( 70 | "--uncompleted", 71 | "-u", 72 | dest="state", 73 | nargs=0, 74 | action=set_value(False), 75 | help="filter by uncompleted todos", 76 | ) 77 | list_parser.add_argument( 78 | "--interactive", "-i", action="store_true", help="toggle interactive mode" 79 | ) 80 | list_parser.set_defaults(command=COMMANDS.GET_GROUP) 81 | list_parser.usage = ( 82 | "td group [name]\n td group [name] list\n " 83 | "td group [name] ls\n td group [name] l" 84 | ) 85 | list_parser.epilog = "`td group [name]` is the shortcut to `td group [name] list`" 86 | list_parser.description = "list group's todos" 87 | -------------------------------------------------------------------------------- /todo/parser/subparsers/initialize_config.py: -------------------------------------------------------------------------------- 1 | from todo.constants import COMMANDS 2 | from todo.parser.base import BaseParser 3 | 4 | 5 | class InitializeConfigParser(BaseParser): 6 | """ 7 | usage: td init-config 8 | td ic 9 | 10 | initialize config 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | """ 15 | 16 | command = COMMANDS.INITIALIZE_CONFIG 17 | -------------------------------------------------------------------------------- /todo/parser/subparsers/list_groups.py: -------------------------------------------------------------------------------- 1 | from todo.constants import COMMANDS 2 | from todo.parser.base import BaseParser, set_value 3 | 4 | 5 | class ListGroupsParser(BaseParser): 6 | """ 7 | usage: td list-groups [--completed] [--uncompleted] 8 | td lg [-c] [-u] 9 | 10 | list groups 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | --completed, -c filter by completed groups 15 | --uncompleted, -u filter by uncompleted groups 16 | """ 17 | 18 | command = COMMANDS.LIST_GROUPS 19 | 20 | def _add_arguments(self): 21 | self.parser.add_argument( 22 | "--completed", 23 | "-c", 24 | dest="state", 25 | nargs=0, 26 | action=set_value(True), 27 | help="filter by completed groups", 28 | ) 29 | self.parser.add_argument( 30 | "--uncompleted", 31 | "-u", 32 | dest="state", 33 | nargs=0, 34 | action=set_value(False), 35 | help="filter by uncompleted groups", 36 | ) 37 | -------------------------------------------------------------------------------- /todo/parser/subparsers/list_todos.py: -------------------------------------------------------------------------------- 1 | from ast import literal_eval 2 | 3 | from todo.constants import COMMANDS 4 | from todo.parser.base import BaseParser, set_value 5 | from todo.settings import config 6 | 7 | 8 | class ListTodosParser(BaseParser): 9 | """ 10 | usage: td [--completed] [--uncompleted] [--raw] [--group GROUP] [--interactive] 11 | td l [-c] [-u] [-r] [-g GROUP] [-i] 12 | td ls [-c] [-u] [-r] [-g GROUP] [-i] 13 | td list [-c] [-u] [-r] [-g GROUP] [-i] 14 | 15 | list todos 16 | 17 | optional arguments: 18 | -h, --help show this help message and exit 19 | --raw, -r only show todos 20 | --completed, -c filter by completed todos 21 | --uncompleted, -u filter by uncompleted todos 22 | --group GROUP, -g GROUP 23 | filter by name of group 24 | --interactive, -i toggle interactive mode 25 | 26 | `td` is the shortcut to `td list` 27 | """ 28 | 29 | command = COMMANDS.LIST_TODOS 30 | 31 | def _add_arguments(self): 32 | self.parser.add_argument( 33 | "--completed", 34 | "-c", 35 | dest="state", 36 | nargs=0, 37 | default=bool(literal_eval(config["completed"])) if config.get("completed") else None, 38 | action=set_value(True), 39 | help="filter by completed todos", 40 | ) 41 | self.parser.add_argument( 42 | "--uncompleted", 43 | "-u", 44 | dest="state", 45 | nargs=0, 46 | action=set_value(False), 47 | help="filter by uncompleted todos", 48 | ) 49 | self.parser.add_argument( 50 | "--raw", 51 | "-r", 52 | dest="raw", 53 | nargs=0, 54 | action=set_value(True), 55 | help="only show todos", 56 | ) 57 | self.parser.add_argument("--group", "-g", action="store", help="filter by name of group") 58 | self.parser.add_argument( 59 | "--interactive", "-i", action="store_true", help="toggle interactive mode" 60 | ) 61 | self.parser.usage = "td [--completed] [--uncompleted] [--group GROUP] [--interactive]" 62 | -------------------------------------------------------------------------------- /todo/parser/subparsers/todo.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from todo.constants import COMMANDS 4 | from todo.parser.base import BaseParser 5 | 6 | 7 | class TodoParser(BaseParser): 8 | """ 9 | usage: td [id] {get,delete,uncomplete,complete,edit} ... 10 | 11 | manage todo 12 | 13 | positional arguments: 14 | id the id of the todo 15 | {...} commands 16 | get (g) show todo's details 17 | delete (d) delete todo 18 | uncomplete (u) uncomplete todo 19 | complete (c) complete todo 20 | edit (e) edit todo 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | 25 | `td [id]` defaults to `td [id] get` 26 | You don't have to specify the whole `id`, a substring will do 27 | """ 28 | 29 | command = COMMANDS.GET_TODO 30 | 31 | def _add_arguments(self): 32 | self.parser.add_argument("id", action="store", help="the id of the todo") 33 | subparser = self.parser.add_subparsers(dest="command", help="commands") 34 | 35 | get_parser = self._add_parser(subparser, "get", aliases=["g"], help="get todo") 36 | get_parser.set_defaults(command=COMMANDS.GET_TODO) 37 | get_parser.usage = "td [id]\n td [id] get\n td [id] g" 38 | get_parser.epilog = "`td [id]` is the shortcut to `td [id] get`" 39 | get_parser.description = "show todo's details" 40 | 41 | delete_parser = self._add_parser(subparser, "delete", aliases=["d"], help="delete todo") 42 | delete_parser.add_argument( 43 | "--yes", 44 | "-y", 45 | dest="skip_prompt", 46 | action="store_true", 47 | help="skip yes/no prompt when deleting todo", 48 | ) 49 | delete_parser.set_defaults(command=COMMANDS.DELETE_TODO) 50 | delete_parser.usage = "td [id] delete [-yes]\n td [id] d [-y]" 51 | delete_parser.description = "delete todo" 52 | 53 | uncomplete_parser = self._add_parser( 54 | subparser, "uncomplete", aliases=["u"], help="uncomplete todo" 55 | ) 56 | uncomplete_parser.set_defaults(command=COMMANDS.UNCOMPLETE_TODO) 57 | uncomplete_parser.usage = "td [id] uncomplete\n td [id] u" 58 | uncomplete_parser.description = "uncomplete todo" 59 | 60 | complete_parser = self._add_parser( 61 | subparser, "complete", aliases=["c"], help="complete todo" 62 | ) 63 | complete_parser.set_defaults(command=COMMANDS.COMPLETE_TODO) 64 | complete_parser.usage = "td [id] complete\n td [id] c" 65 | complete_parser.description = "complete todo" 66 | 67 | edit_parser = self._add_parser( 68 | subparser, 69 | "edit", 70 | aliases=["e"], 71 | help="edit todo", 72 | formatter_class=argparse.RawTextHelpFormatter, 73 | epilog="""If no optional arguments are provided, the todo will be 74 | opened in your editor where you can edit the todo's details. 75 | The editor defaults to `vi`, but you can choose your preferred one be setting: 76 | ``` 77 | [settings] 78 | editor: 79 | ``` 80 | in ~/.td.cfg 81 | """, 82 | ) 83 | edit_parser.add_argument("--name", "-n", action="store", help="update todo's name") 84 | edit_parser.add_argument("--details", "-d", action="store", help="update todo's detail") 85 | edit_parser.add_argument("--group", "-g", action="store", help="set todo's group") 86 | edit_parser.set_defaults(command=COMMANDS.EDIT_TODO) 87 | edit_parser.usage = "td [id] edit [--name NAME] [--details DETAILS]\n td [id] e [-n NAME] [-d DETAILS]" 88 | edit_parser.description = "edit todo" 89 | -------------------------------------------------------------------------------- /todo/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | from .render_error import RenderError 3 | from .render_help import RenderHelp 4 | from .render_input import RenderInput 5 | from .render_output import RenderOutput 6 | from .render_output_with_textwrap import RenderOutputWithTextwrap 7 | -------------------------------------------------------------------------------- /todo/renderers/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from .styles import Fore, Style 4 | 5 | 6 | class Render(ABC): 7 | def _format(self, text, **kwargs): 8 | return text.format( 9 | red=Fore.RED, 10 | green=Fore.GREEN, 11 | blue=Fore.BLUE, 12 | white=Fore.WHITE, 13 | grey=Fore.GREY, 14 | bold=Style.BOLD, 15 | normal=Style.NORMAL, 16 | reset=Style.RESET_ALL, 17 | **kwargs, 18 | ) 19 | 20 | @abstractmethod 21 | def render(self, *args, **kwargs): 22 | pass 23 | -------------------------------------------------------------------------------- /todo/renderers/render_error.py: -------------------------------------------------------------------------------- 1 | from .base import Render 2 | 3 | 4 | class RenderError(Render): 5 | def __init__(self, error, detailed_error, verbose, title): 6 | self.error = error 7 | self.detailed_error = detailed_error 8 | self.verbose = verbose 9 | self.title = title 10 | 11 | def render(self, **kwargs): 12 | if self.verbose: 13 | print(self._format("{red}{title}{reset}:", title=self.title)) 14 | print(self._format(f"{str(self.detailed_error)}%s" % "{reset}", **kwargs)) 15 | else: 16 | print(self._format("{red}{title}{reset}:", title=self.title)) 17 | print(self._format(f"{str(self.error)}%s" % "{reset}", **kwargs)) 18 | if self.detailed_error != self.error and not self.verbose: 19 | print( 20 | self._format( 21 | "\nRun with `{bold}--verbose/-v{reset}` to get more detailed error message", 22 | **kwargs, 23 | ) 24 | ) 25 | -------------------------------------------------------------------------------- /todo/renderers/render_help.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | from .base import Render 4 | 5 | 6 | class RenderHelp(Render): 7 | def __init__(self, text): 8 | self.text = text 9 | 10 | def render(self, **kwargs): 11 | print(dedent(self.text).strip()) 12 | -------------------------------------------------------------------------------- /todo/renderers/render_input.py: -------------------------------------------------------------------------------- 1 | from .base import Render 2 | 3 | 4 | class RenderInput(Render): 5 | def __init__(self, string_to_format): 6 | self.string_to_format = string_to_format 7 | 8 | def render(self, **kwargs): 9 | return input(self._format(f"{self.string_to_format}%s" % "{reset}", **kwargs)).lower() 10 | -------------------------------------------------------------------------------- /todo/renderers/render_output.py: -------------------------------------------------------------------------------- 1 | from .base import Render 2 | 3 | 4 | class RenderOutput(Render): 5 | def __init__(self, string_to_format): 6 | self.string_to_format = string_to_format 7 | 8 | def render(self, **kwargs): 9 | print(self._format(f"{self.string_to_format}%s" % "{reset}", **kwargs)) 10 | -------------------------------------------------------------------------------- /todo/renderers/render_output_with_textwrap.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from todo.utils import get_terminal_size 4 | 5 | from .base import Render 6 | 7 | 8 | class RenderOutputWithTextwrap(Render): 9 | def __init__(self, prefix, text_to_wrap): 10 | self.prefix = prefix 11 | self.text_to_wrap = text_to_wrap 12 | 13 | def render(self, **kwargs): 14 | kwargs.setdefault("subsequent_indent", " " * 10) 15 | 16 | cols, _ = get_terminal_size() 17 | 18 | wrapper = textwrap.TextWrapper( 19 | initial_indent=self._format(self.prefix, **kwargs), 20 | width=cols, 21 | subsequent_indent=kwargs.get("subsequent_indent"), 22 | ) 23 | print(wrapper.fill(self._format(f"{self.text_to_wrap}%s" % "{reset}", **kwargs))) 24 | -------------------------------------------------------------------------------- /todo/renderers/styles/__init__.py: -------------------------------------------------------------------------------- 1 | from .ansi_fore import AnsiFore 2 | from .ansi_style import AnsiStyle 3 | 4 | Fore = AnsiFore() 5 | Style = AnsiStyle() 6 | -------------------------------------------------------------------------------- /todo/renderers/styles/ansi_fore.py: -------------------------------------------------------------------------------- 1 | from .base import AnsiCodes 2 | 3 | 4 | class AnsiFore(AnsiCodes): 5 | RED = 31 6 | GREEN = 32 7 | BLUE = 34 8 | WHITE = 37 9 | GREY = 90 10 | -------------------------------------------------------------------------------- /todo/renderers/styles/ansi_style.py: -------------------------------------------------------------------------------- 1 | from .base import AnsiCodes 2 | 3 | 4 | class AnsiStyle(AnsiCodes): 5 | RESET_ALL = 0 6 | BOLD = 1 7 | NORMAL = 22 8 | -------------------------------------------------------------------------------- /todo/renderers/styles/base.py: -------------------------------------------------------------------------------- 1 | CSI = "\033[" 2 | 3 | 4 | def code_to_chars(code): 5 | return CSI + str(code) + "m" 6 | 7 | 8 | class AnsiCodes(object): 9 | def __init__(self): 10 | for name in dir(self): 11 | if not name.startswith("_"): 12 | value = getattr(self, name) 13 | setattr(self, name, code_to_chars(value)) 14 | -------------------------------------------------------------------------------- /todo/services/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | from pathlib import Path 4 | from urllib.request import pathname2url 5 | 6 | from todo.settings import config, get_home 7 | 8 | from .group import GroupService 9 | from .todo import TodoService 10 | 11 | 12 | class Service: 13 | __slots__ = ("connection", "cursor") 14 | 15 | service_dict = {"todo": TodoService, "group": GroupService} 16 | 17 | def __init__(self): 18 | db_uri = "file:{}".format(pathname2url(self._get_database_path())) 19 | 20 | try: 21 | self.connection = sqlite3.connect("{}?mode=rw".format(db_uri), uri=True) 22 | self.cursor = self.connection.cursor() 23 | except sqlite3.OperationalError: 24 | self._initialise_database(db_uri) 25 | self._link_services() 26 | 27 | self.cursor.execute("PRAGMA foreign_keys = ON") 28 | 29 | def _get_database_path(self): 30 | home_dir, prefix = get_home() 31 | database_name = f"{prefix}{config['database_name']}.db" 32 | 33 | return os.path.expanduser(Path.joinpath(home_dir, database_name)) 34 | 35 | def _initialise_database(self, db_uri): 36 | self.connection = sqlite3.connect(db_uri, uri=True) 37 | self.cursor = self.connection.cursor() 38 | for _, cls in self.service_dict.items(): 39 | cls.initialise_table(self) 40 | self.connection.commit() 41 | 42 | def _link_services(self): 43 | for prop, service in self.service_dict.items(): 44 | setattr(Service, prop, service(self.connection, self.cursor)) 45 | 46 | def __enter__(self): 47 | return self 48 | 49 | def __exit__(self, exc_type, exc_value, traceback): 50 | if exc_type is not None: 51 | return False 52 | self.connection.close() 53 | -------------------------------------------------------------------------------- /todo/services/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from todo.utils import to_lower 4 | 5 | GLOBAL = "global" 6 | 7 | 8 | class BaseService(ABC): 9 | __slots__ = ("connection", "cursor") 10 | 11 | def __init__(self, connection, cursor): 12 | self.connection = connection 13 | self.cursor = cursor 14 | 15 | @abstractmethod 16 | def initialise_table(self): 17 | pass 18 | 19 | def _interpret_group_name(self, name): 20 | if self._is_global(name): 21 | return None 22 | return to_lower(name) 23 | 24 | def _is_global(self, name): 25 | return name is None or to_lower(name) == GLOBAL 26 | -------------------------------------------------------------------------------- /todo/services/group.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from todo.exceptions import TodoException 4 | from todo.services.base import GLOBAL, BaseService 5 | from todo.settings import config, get_project_config 6 | 7 | 8 | class GroupService(BaseService): 9 | def initialise_table(self): 10 | self.cursor.execute( 11 | """ 12 | CREATE TABLE "group"( 13 | name TEXT PRIMARY KEY NOT NULL, 14 | in_use BOOLEAN NOT NULL DEFAULT 0 15 | ); 16 | """ 17 | ) 18 | 19 | # POST 20 | def add(self, name): 21 | group_name = self._interpret_group_name(name) 22 | if group_name is None: 23 | raise TodoException("`{bold}{reset}` already exists." % GLOBAL) 24 | 25 | self.cursor.execute( 26 | """ 27 | INSERT INTO "group" (name) 28 | VALUES (?); 29 | """, 30 | (group_name,), 31 | ) 32 | self.connection.commit() 33 | return group_name 34 | 35 | # DELETE 36 | def delete(self, name): 37 | group_name = self._interpret_group_name(name) 38 | if group_name is None: 39 | raise TodoException("`{bold}{reset}` can't be deleted." % GLOBAL) 40 | self.cursor.execute( 41 | """ 42 | DELETE FROM "group" 43 | WHERE name = ?; 44 | """, 45 | (group_name,), 46 | ) 47 | self.connection.commit() 48 | 49 | # PUT 50 | def edit_name(self, new_name, old_name): 51 | self.cursor.execute( 52 | """ 53 | UPDATE "group" 54 | SET name = ? 55 | WHERE name = ?; 56 | """, 57 | (self._interpret_group_name(new_name), self._interpret_group_name(old_name)), 58 | ) 59 | self.connection.commit() 60 | 61 | def use(self, name): 62 | group = self.get(name) 63 | 64 | if group is None: 65 | raise TodoException(" not found".format(name=name)) 66 | 67 | self.cursor.execute( 68 | """ 69 | UPDATE "group" 70 | SET in_use = 0 71 | """ 72 | ) 73 | 74 | self.cursor.execute( 75 | """ 76 | UPDATE "group" 77 | SET in_use = 1 78 | WHERE name = ?; 79 | """, 80 | (group[0],), 81 | ) 82 | self.connection.commit() 83 | 84 | # GET 85 | def get(self, name): 86 | group_name = self._interpret_group_name(name) 87 | if group_name is None: 88 | group = (None,) 89 | else: 90 | group = self.cursor.execute( 91 | """ 92 | SELECT IFNULL(?, ?) 93 | FROM "group" 94 | WHERE name = ? OR ? IS NULL; 95 | """, 96 | (group_name, GLOBAL, group_name, group_name), 97 | ).fetchone() 98 | if group is None: 99 | return None 100 | 101 | self.cursor.execute( 102 | """ 103 | SELECT COUNT(id) AS items, 104 | COALESCE(SUM(completed = 0), 0) AS uncompleted, 105 | COALESCE(SUM(completed = 1), 0) AS completed 106 | FROM todo 107 | WHERE group_name = ? OR ? IS NULL; 108 | """, 109 | (group_name, group_name), 110 | ) 111 | return group + self.cursor.fetchone() 112 | 113 | def get_active_group(self): 114 | if config["group"]: 115 | group = self.get(config["group"]) 116 | if group is None: 117 | raise TodoException( 118 | "{bold}{reset} does not exist, falling back to currently active group" 119 | % config["group"], 120 | "Your config file at {bold}%s{reset} tries to\noverride the " 121 | % (get_project_config(Path.cwd()) or "~") 122 | + "default group with `{bold}%s{reset}`, but " % config["group"] 123 | + "{bold}{reset} does not exist." % config["group"], 124 | "WARNING", 125 | ) 126 | return group 127 | self.cursor.execute( 128 | """ 129 | SELECT name 130 | FROM "group" 131 | WHERE in_use = 1; 132 | """ 133 | ) 134 | active_group = self.cursor.fetchone() or (None,) 135 | return self.get(*active_group) 136 | 137 | def get_all(self, completed=None): 138 | self.cursor.execute( 139 | """ 140 | SELECT IFNULL(todos.group_name, 'UNGROUPED'), todos.items, todos.uncompleted, todos.completed 141 | FROM ( 142 | SELECT group_name, 143 | COUNT(*) as items, 144 | SUM(completed = 0) as uncompleted, 145 | SUM(completed = 1) as completed 146 | FROM todo 147 | GROUP BY group_name 148 | ) todos 149 | WHERE (todos.uncompleted > 0 AND ? = 0) 150 | OR (todos.uncompleted = 0 AND ? = 1) 151 | OR ? IS NULL 152 | UNION ALL 153 | SELECT g2.name, 0, 0, 0 154 | FROM "group" g2 155 | LEFT OUTER JOIN todo ON todo.group_name = g2.name 156 | WHERE todo.group_name IS NULL 157 | AND (? = 1 OR ? IS NULL); 158 | """, 159 | (completed,) * 5, 160 | ) 161 | return self.cursor.fetchall() 162 | -------------------------------------------------------------------------------- /todo/services/todo.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from todo.services.base import BaseService 4 | from todo.utils import generate_random_int 5 | 6 | 7 | class TodoService(BaseService): 8 | def initialise_table(self): 9 | self.cursor.execute( 10 | """ 11 | CREATE TABLE todo( 12 | id TEXT PRIMARY KEY NOT NULL, 13 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | name TEXT NOT NULL, 16 | details TEXT, 17 | completed BOOLEAN NOT NULL DEFAULT 0, 18 | group_name TEXT, 19 | FOREIGN KEY (group_name) REFERENCES "group" (name) ON DELETE CASCADE 20 | ); 21 | """ 22 | ) 23 | self.cursor.execute( 24 | """ 25 | CREATE TRIGGER update_modify_on_todo_update AFTER UPDATE ON todo 26 | BEGIN 27 | UPDATE todo 28 | SET modified = datetime('now') 29 | WHERE id = NEW.id; 30 | END; 31 | """ 32 | ) 33 | 34 | # POST 35 | def add(self, name, details, group, completed): 36 | id = generate_random_int() 37 | group_name = self._interpret_group_name(group) 38 | self.cursor.execute( 39 | """ 40 | INSERT INTO todo (id, name, details, group_name, completed) 41 | VALUES (?, ?, ?, ?, ?); 42 | """, 43 | (id, name, details, group_name, completed), 44 | ) 45 | self.connection.commit() 46 | return id 47 | 48 | # DELETE 49 | def delete(self, id): 50 | self.cursor.execute( 51 | """ 52 | DELETE FROM todo 53 | WHERE id = ?; 54 | """, 55 | (id,), 56 | ) 57 | self.connection.commit() 58 | 59 | # PUT 60 | def complete(self, id): 61 | self.cursor.execute( 62 | """ 63 | UPDATE todo 64 | SET completed = 1 65 | WHERE id = ?; 66 | """, 67 | (id,), 68 | ) 69 | self.connection.commit() 70 | 71 | def uncomplete(self, id): 72 | self.cursor.execute( 73 | """ 74 | UPDATE todo 75 | SET completed = 0 76 | WHERE id = ?; 77 | """, 78 | (id,), 79 | ) 80 | self.connection.commit() 81 | 82 | def set_group(self, id, group): 83 | group_name = self._interpret_group_name(group) 84 | self.cursor.execute( 85 | """ 86 | UPDATE todo 87 | SET group_name = ? 88 | WHERE id = ?; 89 | """, 90 | (group_name, id), 91 | ) 92 | self.connection.commit() 93 | 94 | def edit_details(self, id, details): 95 | self.cursor.execute( 96 | """ 97 | UPDATE todo 98 | SET details = ? 99 | WHERE id = ?; 100 | """, 101 | (details, id), 102 | ) 103 | self.connection.commit() 104 | 105 | def edit_name(self, id, name): 106 | self.cursor.execute( 107 | """ 108 | UPDATE todo 109 | SET name = ? 110 | WHERE id = ?; 111 | """, 112 | (name, id), 113 | ) 114 | self.connection.commit() 115 | 116 | # GET 117 | def get(self, id, group): 118 | self.cursor.execute( 119 | """ 120 | SELECT id, group_name, name, details, completed 121 | FROM todo 122 | WHERE id LIKE ('%' || ? || '%') 123 | ORDER BY group_name = ? DESC, completed, modified DESC; 124 | """, 125 | (id, group), 126 | ) 127 | return self.cursor.fetchone() 128 | 129 | def get_all(self, group=None, completed=None): 130 | group_name = self._interpret_group_name(group) 131 | self.cursor.execute( 132 | """ 133 | SELECT id, name, details, completed 134 | FROM todo 135 | WHERE (completed = ? OR ? IS NULL) AND 136 | (group_name = ? OR ? IS NULL) 137 | ORDER BY completed, modified DESC; 138 | """, 139 | (completed, completed, group_name, group_name), 140 | ) 141 | return self.cursor.fetchall() 142 | -------------------------------------------------------------------------------- /todo/settings.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | from functools import lru_cache 4 | from pathlib import Path 5 | from typing import Optional, Tuple 6 | 7 | from todo.exceptions import TodoException 8 | 9 | CONFIG_SECTION = "settings" 10 | DEFAULT_CONFIG = {"database_name": "todo", "editor": "vi", "group": None, "format": "tmp"} 11 | EXAMPLE_CONFIG = """[settings] 12 | group: {group} 13 | completed: 0 14 | """ 15 | 16 | 17 | @lru_cache() 18 | def get_project_config(filepath): 19 | """returns the absolute path of nearest config""" 20 | config_file = Path.joinpath(filepath, ".td.cfg") 21 | 22 | if Path.home() >= filepath: 23 | return None 24 | elif Path.exists(config_file): 25 | return config_file 26 | else: 27 | return get_project_config(filepath.parent) 28 | 29 | 30 | @lru_cache() 31 | def get_home() -> Tuple[Path, str]: 32 | # try from TD_CLI_HOME environment variable 33 | td_cli_env: Optional[str] = os.environ.get("TD_CLI_HOME") 34 | if td_cli_env: 35 | td_cli_env_dir: Path = Path.expanduser(Path(td_cli_env)) 36 | if not Path.exists(td_cli_env_dir): 37 | raise TodoException( 38 | f'TD_CLI_HOME environment variable set to "{td_cli_env_dir}", but directory does not exist' 39 | ) 40 | 41 | return (td_cli_env_dir, "") 42 | 43 | # try from XDG_CONFIG_HOME environment variable 44 | xdg_config_home: Optional[str] = os.environ.get("XDG_CONFIG_HOME") 45 | if xdg_config_home: 46 | xdg_config_home_dir: Path = Path.expanduser(Path(xdg_config_home)) 47 | if not Path.exists(xdg_config_home_dir): 48 | raise TodoException( 49 | f'XDG_CONFIG_HOME environment variable set to "{xdg_config_home}", but directory does not exist' 50 | ) 51 | 52 | config_dir = Path.joinpath(xdg_config_home_dir, "td-cli") 53 | if not config_dir.exists(): 54 | Path.mkdir(config_dir) 55 | return (config_dir, "") 56 | 57 | # fallback to home directory 58 | return (Path.home(), ".") 59 | 60 | 61 | @lru_cache() 62 | def _get_config(): 63 | settings = DEFAULT_CONFIG 64 | 65 | def _update_from_config(config_file): 66 | config = configparser.ConfigParser() 67 | config.read(config_file) 68 | if config.has_section(CONFIG_SECTION): 69 | user_settings = dict(config.items(CONFIG_SECTION)) 70 | settings.update(user_settings) 71 | 72 | # from environment 73 | environment_editor = os.environ.get("EDITOR") 74 | if environment_editor: 75 | settings["editor"] = environment_editor 76 | 77 | # from root config 78 | home_dir, prefix = get_home() 79 | config_file = Path.joinpath(home_dir, f"{prefix}todo.cfg") 80 | if Path.exists(config_file): 81 | _update_from_config(config_file) 82 | 83 | settings["group"] = None 84 | 85 | # from project config 86 | project_config_file = get_project_config(Path.cwd()) 87 | if project_config_file: 88 | _update_from_config(project_config_file) 89 | 90 | return settings 91 | 92 | 93 | config = _get_config() 94 | -------------------------------------------------------------------------------- /todo/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import random 2 | import tempfile 3 | from os import get_terminal_size as os_get_terminal_size 4 | from subprocess import call 5 | 6 | from pkg_resources import get_distribution 7 | 8 | from todo.settings import config 9 | 10 | 11 | def generate_random_int(): 12 | return "%06i" % random.randrange(10**6) 13 | 14 | 15 | def get_user_input(editor, initial_message=b""): 16 | with tempfile.NamedTemporaryFile(suffix=f".{config['format']}") as tf: 17 | tf.write(initial_message) 18 | tf.flush() 19 | call([editor, "+set backupcopy=yes", tf.name]) 20 | 21 | tf.seek(0) 22 | edited_message = tf.read() 23 | return edited_message.decode("utf-8").strip() 24 | 25 | 26 | def singular_or_plural(n): 27 | return "" if n == 1 else "s" 28 | 29 | 30 | def to_lower(string): 31 | return string.strip().lower() 32 | 33 | 34 | def interpret_state(state): 35 | if state is None: 36 | return "" 37 | elif state: 38 | return " completed" 39 | return " uncompleted" 40 | 41 | 42 | def docstring(*sub): 43 | def dec(obj): 44 | obj.__doc__ = obj.__doc__ % sub 45 | return obj 46 | 47 | return dec 48 | 49 | 50 | def get_version(): 51 | return get_distribution("td-cli").version 52 | 53 | 54 | def strikethrough(string): 55 | return "\u0336".join("{string}\u0336".format(string=string)) 56 | 57 | 58 | def hellip_prefix(string, sub_length): 59 | return "…" + string[sub_length:] 60 | 61 | 62 | def hellip_postfix(string, sub_length): 63 | return string[:sub_length] + "…" 64 | 65 | 66 | def get_terminal_size(): 67 | try: 68 | return os_get_terminal_size() 69 | except OSError: 70 | return 80, 43 71 | -------------------------------------------------------------------------------- /todo/utils/menu/__init__.py: -------------------------------------------------------------------------------- 1 | import curses 2 | import os 3 | from collections import namedtuple 4 | 5 | from todo.constants import COMMAND_MODES 6 | from todo.constants import INTERACTIVE_COMMANDS as COMMANDS 7 | from todo.exceptions import TodoException 8 | from todo.utils import get_terminal_size, hellip_postfix, strikethrough 9 | 10 | from .horizontal_tracker import HorizontalTracker 11 | 12 | X_OFFSET = 2 13 | Y_OFFSET = 4 14 | MARGIN = 2 15 | NEXT_LINE = 1 16 | 17 | NUMBER_OF_COMMANDS = 5 18 | 19 | 20 | class Menu: 21 | __slots__ = ("stdscr", "color", "cols") 22 | 23 | commands = namedtuple( # type: ignore 24 | "Command", 25 | ( 26 | COMMANDS.ADD, 27 | COMMANDS.DELETE, 28 | COMMANDS.DOWN, 29 | COMMANDS.EDIT, 30 | COMMANDS.ENTER, 31 | COMMANDS.ESCAPE, 32 | COMMANDS.QUIT, 33 | COMMANDS.RECOVER, 34 | COMMANDS.TOGGLE, 35 | COMMANDS.UP, 36 | ), 37 | )( 38 | add=(97,), 39 | delete=(100,), 40 | down=(curses.KEY_DOWN, 106), 41 | edit=(101,), 42 | enter=(10,), 43 | escape=(27,), 44 | quit=(113, 27), 45 | recover=(114,), 46 | toggle=(32,), 47 | up=(curses.KEY_UP, 107), 48 | ) 49 | 50 | class Color: 51 | __slots__ = ("blue", "grey") 52 | 53 | def __init__(self): 54 | try: 55 | # init colors 56 | curses.start_color() 57 | curses.use_default_colors() 58 | 59 | # add blue 60 | curses.init_pair(1, curses.COLOR_BLUE, -1) 61 | self.blue = curses.color_pair(True) 62 | 63 | # add grey 64 | curses.init_pair(2, 8, -1) 65 | self.grey = curses.color_pair(2) 66 | except Exception: 67 | self.blue = 1 68 | self.grey = 1 69 | 70 | def __init__(self): 71 | os.environ.setdefault("ESCDELAY", "25") 72 | self.cols, _ = get_terminal_size() 73 | self.stdscr = curses.initscr() 74 | try: 75 | self._setup_screen() 76 | except Exception as e: 77 | self._reset_screen() 78 | raise TodoException("Error occurred, could not initialize menu", e) 79 | self.color = self.Color() 80 | 81 | def __enter__(self): 82 | return self 83 | 84 | def __exit__(self, exc_type, exc_value, traceback): 85 | self._reset_screen() 86 | 87 | if exc_type is not None: 88 | return False 89 | return True 90 | 91 | def _setup_screen(self): 92 | # turn off automatic echoing of keys to the screen 93 | curses.noecho() 94 | 95 | # react to keys instantly, without requiring the Enter key to be pressed 96 | curses.cbreak() 97 | 98 | # enable return of special keys 99 | self.stdscr.keypad(True) 100 | 101 | # remove curser 102 | curses.curs_set(False) 103 | 104 | def _reset_screen(self): 105 | if hasattr(self, "stdscr"): 106 | # reverse the curses-friendly terminal settings 107 | self.stdscr.keypad(False) 108 | curses.nocbreak() 109 | curses.echo() 110 | 111 | # restore the terminal to its original operating mode. 112 | curses.endwin() 113 | 114 | def _clear_commands(self, offset, number_of_commands=NUMBER_OF_COMMANDS): 115 | # clear screen for commands 116 | for i in range(number_of_commands): 117 | self.stdscr.move(offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * (i + 2), 0) 118 | self.clear_leftovers() 119 | 120 | def _render_add_commands(self, offset): 121 | # clear screen for previous commands 122 | self._clear_commands(offset - 2, NUMBER_OF_COMMANDS + 1) 123 | 124 | # save 125 | self.stdscr.addstr( 126 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 1, 127 | X_OFFSET + MARGIN, 128 | "enter", 129 | curses.A_BOLD | self.color.grey, 130 | ) 131 | self.clear_leftovers() 132 | self.stdscr.addstr( 133 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 1, 134 | X_OFFSET + MARGIN * 5, 135 | "to save new title", 136 | self.color.grey, 137 | ) 138 | 139 | # abort 140 | self.stdscr.addstr( 141 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 142 | X_OFFSET + MARGIN, 143 | "escape", 144 | curses.A_BOLD | self.color.grey, 145 | ) 146 | self.clear_leftovers() 147 | self.stdscr.addstr( 148 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 149 | X_OFFSET + MARGIN * 5, 150 | "to exit edit mode without saving", 151 | self.color.grey, 152 | ) 153 | 154 | def _render_edit_commands(self, offset): 155 | # clear screen for previous commands 156 | self._clear_commands(offset) 157 | 158 | # save 159 | self.stdscr.addstr( 160 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE, 161 | X_OFFSET + MARGIN, 162 | "enter", 163 | curses.A_BOLD | self.color.grey, 164 | ) 165 | self.clear_leftovers() 166 | self.stdscr.addstr( 167 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE, 168 | X_OFFSET + MARGIN * 5, 169 | "to save new title", 170 | self.color.grey, 171 | ) 172 | 173 | # abort 174 | self.stdscr.addstr( 175 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 176 | X_OFFSET + MARGIN, 177 | "escape", 178 | curses.A_BOLD | self.color.grey, 179 | ) 180 | self.clear_leftovers() 181 | self.stdscr.addstr( 182 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 183 | X_OFFSET + MARGIN * 5, 184 | "to exit edit mode without saving", 185 | self.color.grey, 186 | ) 187 | 188 | def _render_empty_commands(self, offset): 189 | # clear screen for previous commands 190 | self._clear_commands(offset) 191 | 192 | # add 193 | self.stdscr.addstr( 194 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 1, 195 | X_OFFSET + MARGIN, 196 | "a", 197 | curses.A_BOLD | self.color.grey, 198 | ) 199 | self.clear_leftovers() 200 | self.stdscr.addstr( 201 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 1, 202 | X_OFFSET + MARGIN * 5, 203 | "to add a todo", 204 | self.color.grey, 205 | ) 206 | 207 | # quit 208 | self.stdscr.addstr( 209 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 210 | X_OFFSET + MARGIN, 211 | "q", 212 | curses.A_BOLD | self.color.grey, 213 | ) 214 | self.clear_leftovers() 215 | self.stdscr.addstr( 216 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 217 | X_OFFSET + MARGIN * 5, 218 | "to quit", 219 | self.color.grey, 220 | ) 221 | 222 | def _render_delete_commands(self, offset): 223 | # clear screen for previous commands 224 | self._clear_commands(offset) 225 | 226 | # add 227 | self.stdscr.addstr( 228 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 1, 229 | X_OFFSET + MARGIN, 230 | "a", 231 | curses.A_BOLD | self.color.grey, 232 | ) 233 | self.clear_leftovers() 234 | self.stdscr.addstr( 235 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 1, 236 | X_OFFSET + MARGIN * 5, 237 | "to add a todo", 238 | self.color.grey, 239 | ) 240 | 241 | # recover 242 | self.stdscr.addstr( 243 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 244 | X_OFFSET + MARGIN, 245 | "r", 246 | curses.A_BOLD | self.color.grey, 247 | ) 248 | self.clear_leftovers() 249 | self.stdscr.addstr( 250 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 251 | X_OFFSET + MARGIN * 5, 252 | "to recover deleted todo", 253 | self.color.grey, 254 | ) 255 | 256 | # quit 257 | self.stdscr.addstr( 258 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 3, 259 | X_OFFSET + MARGIN, 260 | "q", 261 | curses.A_BOLD | self.color.grey, 262 | ) 263 | self.clear_leftovers() 264 | self.stdscr.addstr( 265 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 3, 266 | X_OFFSET + MARGIN * 5, 267 | "to quit", 268 | self.color.grey, 269 | ) 270 | 271 | def _render_default_commands(self, offset): 272 | # clear screen for previous commands 273 | self._clear_commands(offset) 274 | 275 | # add 276 | self.stdscr.addstr( 277 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 1, 278 | X_OFFSET + MARGIN, 279 | "a", 280 | curses.A_BOLD | self.color.grey, 281 | ) 282 | self.clear_leftovers() 283 | self.stdscr.addstr( 284 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 1, 285 | X_OFFSET + MARGIN * 5, 286 | "to add a todo", 287 | self.color.grey, 288 | ) 289 | 290 | # edit 291 | self.stdscr.addstr( 292 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 293 | X_OFFSET + MARGIN, 294 | "e", 295 | curses.A_BOLD | self.color.grey, 296 | ) 297 | self.clear_leftovers() 298 | self.stdscr.addstr( 299 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 2, 300 | X_OFFSET + MARGIN * 5, 301 | "to edit todo", 302 | self.color.grey, 303 | ) 304 | 305 | # delete 306 | self.stdscr.addstr( 307 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 3, 308 | X_OFFSET + MARGIN, 309 | "d", 310 | curses.A_BOLD | self.color.grey, 311 | ) 312 | self.clear_leftovers() 313 | self.stdscr.addstr( 314 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 3, 315 | X_OFFSET + MARGIN * 5, 316 | "to delete todo", 317 | self.color.grey, 318 | ) 319 | 320 | # space 321 | self.stdscr.addstr( 322 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 4, 323 | X_OFFSET + MARGIN, 324 | "space", 325 | curses.A_BOLD | self.color.grey, 326 | ) 327 | self.clear_leftovers() 328 | self.stdscr.addstr( 329 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 4, 330 | X_OFFSET + MARGIN * 5, 331 | "to toggle completed/uncompleted", 332 | self.color.grey, 333 | ) 334 | 335 | # quit 336 | self.stdscr.addstr( 337 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 5, 338 | X_OFFSET + MARGIN, 339 | "q", 340 | curses.A_BOLD | self.color.grey, 341 | ) 342 | self.clear_leftovers() 343 | self.stdscr.addstr( 344 | offset + Y_OFFSET + MARGIN * 2 + NEXT_LINE * 5, 345 | X_OFFSET + MARGIN * 5, 346 | "to quit", 347 | self.color.grey, 348 | ) 349 | 350 | def clear(self): 351 | # clear screen 352 | self.stdscr.clear() 353 | 354 | def clear_leftovers(self): 355 | # clear leftovers in line 356 | self.stdscr.clrtoeol() 357 | 358 | def refresh(self): 359 | self.stdscr.refresh() 360 | 361 | def get_command(self): 362 | command = self.stdscr.getch() 363 | if command in self.commands.add: 364 | return COMMANDS.ADD 365 | elif command in self.commands.delete: 366 | return COMMANDS.DELETE 367 | elif command in self.commands.down: 368 | return COMMANDS.DOWN 369 | elif command in self.commands.edit: 370 | return COMMANDS.EDIT 371 | elif command in self.commands.quit: 372 | return COMMANDS.QUIT 373 | elif command in self.commands.recover: 374 | return COMMANDS.RECOVER 375 | elif command in self.commands.toggle: 376 | return COMMANDS.TOGGLE 377 | elif command in self.commands.up: 378 | return COMMANDS.UP 379 | else: 380 | return None 381 | 382 | def render_header(self, text): 383 | self.stdscr.addstr(Y_OFFSET, X_OFFSET + MARGIN, text, curses.A_BOLD | self.color.blue) 384 | 385 | def render_subheader(self, text): 386 | self.stdscr.addstr(Y_OFFSET + NEXT_LINE, X_OFFSET + MARGIN, text) 387 | self.clear_leftovers() 388 | 389 | def render_todo(self, todo, offset, current_pos, is_deleted): 390 | extra_style = 1 391 | if offset == current_pos: 392 | extra_style = self.color.blue 393 | # render active cursor 394 | self.stdscr.addstr( 395 | offset + Y_OFFSET + MARGIN + NEXT_LINE, 396 | X_OFFSET, 397 | "❯", 398 | curses.A_BOLD | self.color.blue, 399 | ) 400 | else: 401 | # render non-active cursor 402 | self.stdscr.addstr(offset + Y_OFFSET + MARGIN + NEXT_LINE, X_OFFSET, " ") 403 | 404 | if is_deleted or todo[3] is None: 405 | # render empty state 406 | self.stdscr.addstr(offset + Y_OFFSET + MARGIN + NEXT_LINE, X_OFFSET + MARGIN, " ") 407 | else: 408 | self.stdscr.addstr( 409 | offset + Y_OFFSET + MARGIN + NEXT_LINE, 410 | X_OFFSET + MARGIN, 411 | "{completed}".format(completed="✓" if todo[3] else "x"), 412 | extra_style, 413 | ) 414 | 415 | # render todo id 416 | todo_id_text = "{todo_id}".format(todo_id=todo[0]) 417 | self.stdscr.addstr( 418 | offset + Y_OFFSET + MARGIN + NEXT_LINE, 419 | X_OFFSET + MARGIN * 2, 420 | strikethrough(todo_id_text) if is_deleted else todo_id_text, 421 | curses.A_BOLD | extra_style, 422 | ) 423 | 424 | # render todo name 425 | screen_space = self.cols - (X_OFFSET + MARGIN * 5 + 4) 426 | name_length = len(todo[1]) 427 | todo_name_text = "{name}".format( 428 | name=hellip_postfix(todo[1], screen_space) if name_length > screen_space else todo[1] 429 | ) 430 | self.stdscr.addstr( 431 | offset + Y_OFFSET + MARGIN + NEXT_LINE, 432 | X_OFFSET + MARGIN * 5, 433 | ": {name}".format(name=strikethrough(todo_name_text) if is_deleted else todo_name_text), 434 | extra_style, 435 | ) 436 | 437 | # clear leftovers 438 | self.clear_leftovers() 439 | 440 | def render_commands(self, offset, mode=COMMAND_MODES.DEFAULT): 441 | if mode == COMMAND_MODES.ADD: 442 | self._render_add_commands(offset) 443 | elif mode == COMMAND_MODES.EMPTY: 444 | self._render_empty_commands(offset) 445 | elif mode == COMMAND_MODES.DEFAULT: 446 | self._render_default_commands(offset) 447 | elif mode == COMMAND_MODES.DELETE: 448 | self._render_delete_commands(offset) 449 | elif mode == COMMAND_MODES.EDIT: 450 | self._render_edit_commands(offset) 451 | else: 452 | self._clear_commands() 453 | 454 | def edit_text(self, text, offset): # noqa: 901 455 | Y_ORIGIN = offset + Y_OFFSET + MARGIN + NEXT_LINE 456 | X_ORIGIN = X_OFFSET + MARGIN * 5 + 2 457 | 458 | tracker = HorizontalTracker(text, X_ORIGIN, self.cols) 459 | self.stdscr.addstr(Y_ORIGIN, X_ORIGIN, tracker.get_hellip_string(), self.color.blue) 460 | 461 | self.clear_leftovers() 462 | self.refresh() 463 | try: 464 | # show cursor 465 | curses.curs_set(True) 466 | while True: 467 | char_key = self.stdscr.get_wch() 468 | key = char_key if isinstance(char_key, int) else ord(char_key) 469 | 470 | if key in self.commands.enter: 471 | break 472 | elif key in self.commands.escape: 473 | tracker.erase_string() 474 | break 475 | elif key == curses.KEY_LEFT: 476 | tracker.move_left() 477 | elif key == 262: # fn + KEY_LEFT 478 | tracker.move_to_start() 479 | elif key == 360: # fn + KEY_RIGHT 480 | tracker.move_to_end() 481 | elif key == curses.KEY_RIGHT: 482 | tracker.move_right() 483 | elif key == 8 or key == 127 or key == curses.KEY_BACKSPACE: 484 | tracker.delete() 485 | elif key == 330: # fn + KEY_BACKSPACE 486 | tracker.delete_backwards() 487 | elif not curses.keyname(key).startswith(b"KEY_"): 488 | tracker.add(char_key) 489 | 490 | # clear the line and rewrite string 491 | self.stdscr.move(Y_ORIGIN, X_ORIGIN) 492 | self.clear_leftovers() 493 | self.stdscr.addstr(Y_ORIGIN, X_ORIGIN, tracker.get_hellip_string(), self.color.blue) 494 | 495 | # move cursor to correct place and refresh screen 496 | self.stdscr.move(Y_ORIGIN, tracker.get_cursor_pos()) 497 | self.stdscr.refresh() 498 | finally: 499 | curses.curs_set(False) 500 | return tracker.get_string() 501 | -------------------------------------------------------------------------------- /todo/utils/menu/horizontal_tracker.py: -------------------------------------------------------------------------------- 1 | from todo.utils import hellip_postfix, hellip_prefix 2 | 3 | 4 | class HorizontalTracker: 5 | __slots__ = ("_string", "_string_pos", "_cursor_pos", "_x_origin", "_terminal_width") 6 | 7 | def __init__(self, string, x_origin, terminal_width): 8 | string_length = len(string) 9 | 10 | self._string = string 11 | self._string_pos = string_length 12 | self._cursor_pos = min(terminal_width - 1, string_length + x_origin) 13 | self._x_origin = x_origin 14 | self._terminal_width = terminal_width 15 | 16 | @property 17 | def _relative_cursor_pos(self): 18 | return self._cursor_pos - self._x_origin 19 | 20 | @property 21 | def _max_length(self): 22 | return self._terminal_width - self._x_origin 23 | 24 | @property 25 | def _string_length(self): 26 | return len(self._string) 27 | 28 | def move_left(self): 29 | self._string_pos = max(self._string_pos - 1, 0) 30 | if not (self._string_pos > 1 and self._relative_cursor_pos == 2): 31 | self._cursor_pos = max(self._cursor_pos - 1, self._x_origin) 32 | 33 | def move_right(self): 34 | self._string_pos = min(self._string_pos + 1, self._string_length) 35 | if not ( 36 | self._string_pos < self._string_length - 1 37 | and self._relative_cursor_pos == self._max_length - 3 38 | ): 39 | self._cursor_pos = min( 40 | self._cursor_pos + 1, 41 | min(self._string_length + self._x_origin, self._terminal_width - 1), 42 | ) 43 | 44 | def move_to_start(self): 45 | self._string_pos = 0 46 | self._cursor_pos = self._x_origin 47 | 48 | def move_to_end(self): 49 | self._string_pos = self._string_length 50 | self._cursor_pos = min(self._string_length + self._x_origin, self._terminal_width - 1) 51 | 52 | def delete(self): 53 | if self._string_pos > 0: 54 | self._string = self._string[: self._string_pos - 1] + self._string[self._string_pos :] 55 | self._string_pos = max(self._string_pos - 1, 0) 56 | if self._string_pos < self._relative_cursor_pos: 57 | self._cursor_pos = max(self._cursor_pos - 1, self._x_origin) 58 | 59 | def delete_backwards(self): 60 | if self._string_pos < self._string_length: 61 | self._string = self._string[: self._string_pos] + self._string[self._string_pos + 1 :] 62 | if self._string_pos > self._relative_cursor_pos: 63 | self._cursor_pos = min(self._cursor_pos + 1, self._terminal_width - 1) 64 | 65 | def add(self, char): 66 | self._string = self._string[: self._string_pos] + char + self._string[self._string_pos :] 67 | self._string_pos = self._string_pos + 1 68 | if not ( 69 | self._string_pos < self._string_length - 1 70 | and self._relative_cursor_pos == self._max_length - 3 71 | ): 72 | self._cursor_pos = min(self._cursor_pos + 1, self._terminal_width - 1) 73 | 74 | def erase_string(self): 75 | self._string = None 76 | 77 | def get_hellip_string(self): 78 | # make a copy 79 | string = self._string 80 | if self._string_length >= self._max_length: 81 | if self._string_pos > self._relative_cursor_pos: 82 | string = hellip_prefix(string, self._string_pos - self._relative_cursor_pos + 1) 83 | 84 | leftovers = self._string_length - self._string_pos 85 | screen_space = self._max_length - self._relative_cursor_pos 86 | if leftovers >= screen_space: 87 | string = hellip_postfix(string, self._max_length - 2) 88 | return string 89 | 90 | def get_string(self): 91 | return self._string 92 | 93 | def get_cursor_pos(self): 94 | return self._cursor_pos 95 | -------------------------------------------------------------------------------- /todo/utils/menu/vertical_tracker.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from todo.utils import get_terminal_size 4 | 5 | 6 | class VerticalTracker: 7 | __slots__ = ("_rows", "_todos", "_group", "_current_pos", "_cursor", "_deleted_todos") 8 | 9 | PADDING = 15 10 | 11 | Group = namedtuple("Group", ("name", "items", "uncompleted", "completed")) 12 | Todo = namedtuple("Todo", ("id", "name", "details", "completed")) 13 | 14 | def __init__(self, todos, group): 15 | _, rows = get_terminal_size() 16 | start_pos = 0 if len(todos) > 0 else -1 17 | self._rows = rows - self.PADDING 18 | self._todos = todos 19 | self._group = group 20 | self._current_pos = start_pos 21 | self._cursor = start_pos 22 | self._deleted_todos = set() 23 | 24 | @property 25 | def _current_todo(self): 26 | return self._todos[self._current_pos] 27 | 28 | @property 29 | def todos_count(self): 30 | return len(self._todos) 31 | 32 | @property 33 | def todos(self): 34 | start_pos = self._current_pos - self._cursor 35 | return self._todos[start_pos : (start_pos + self._rows)] 36 | 37 | @property 38 | def current_todo(self): 39 | if self.todos_count > 0: 40 | return self.Todo(*self._current_todo) 41 | return self.Todo(*((None,) * len(self.Todo._fields))) 42 | 43 | @property 44 | def group(self): 45 | return self.Group(*self._group) 46 | 47 | @property 48 | def commands_offset(self): 49 | return min(self._rows, self.todos_count) 50 | 51 | @property 52 | def index(self): 53 | return self._cursor 54 | 55 | @property 56 | def rows(self): 57 | return self._rows 58 | 59 | def is_deleted(self, id): 60 | return id in self._deleted_todos 61 | 62 | def move_down(self): 63 | self._current_pos += 1 64 | if self._current_pos == self.todos_count: # on last todo 65 | self._cursor = 0 66 | self._current_pos = 0 67 | elif not (self._cursor + 2 == self._rows and self._current_pos != self.todos_count - 1): 68 | self._cursor += 1 69 | 70 | def move_up(self): 71 | self._current_pos -= 1 72 | if self._current_pos < 0: 73 | self._cursor = self.commands_offset - 1 74 | self._current_pos = self.todos_count - 1 75 | elif not (self._cursor - 1 == 0 and self._current_pos != 0): 76 | self._cursor -= 1 77 | 78 | def recover(self): 79 | self._deleted_todos.discard(self._current_todo[0]) 80 | if self._current_todo[3]: 81 | self._group = (self._group[0], self._group[1] + 1, self._group[2], self._group[3] + 1) 82 | else: 83 | self._group = (self._group[0], self._group[1] + 1, self._group[2] + 1, self._group[3]) 84 | 85 | def toggle(self, todo_service): 86 | # toggle todo 87 | if self._current_todo[3]: 88 | # uncomplete todo 89 | todo_service.uncomplete(self._current_todo[0]) 90 | self._group = self._group[:2] + (self._group[2] + 1, self._group[3] - 1) 91 | else: 92 | # complete todo 93 | todo_service.complete(self._current_todo[0]) 94 | self._group = self._group[:2] + (self._group[2] - 1, self._group[3] + 1) 95 | # update list 96 | self._todos[self._current_pos] = self._current_todo[:3] + (not self._current_todo[3],) 97 | 98 | def add(self, todo): 99 | # add empty line 100 | self._current_pos += 1 101 | self._todos = self._todos[: self._current_pos] + [todo] + self._todos[self._current_pos :] 102 | if self._cursor + 2 < self._rows: 103 | self._cursor += 1 104 | 105 | def remove(self): 106 | self._todos = self._todos[: self._current_pos] + self._todos[self._current_pos + 1 :] 107 | self._current_pos -= 1 108 | if self._cursor + 2 < self._rows: 109 | self._cursor -= 1 110 | 111 | def update(self, name, todo_service): 112 | id = todo_service.add(name, name, self._group[0], False) 113 | self._todos[self._current_pos] = (id, name, name, False) 114 | self._group = (self._group[0], self._group[1] + 1, self._group[2] + 1, self._group[3]) 115 | 116 | def edit(self, new_name, todo_service): 117 | if new_name is not None: 118 | todo_service.edit_name(self._current_todo[0], new_name) 119 | self._todos[self._current_pos] = (self._current_todo[0], new_name) + self._current_todo[ 120 | 2: 121 | ] 122 | 123 | def mark_deleted(self): 124 | self._deleted_todos.add(self._current_todo[0]) 125 | if self._current_todo[3]: 126 | self._group = (self._group[0], self._group[1] - 1, self._group[2], self._group[3] - 1) 127 | else: 128 | self._group = (self._group[0], self._group[1] - 1, self._group[2] - 1, self._group[3]) 129 | 130 | def delete_todos(self, todo_service): 131 | for deleted_todo in self._deleted_todos: 132 | todo_service.delete(deleted_todo) 133 | --------------------------------------------------------------------------------