├── .github └── workflows │ ├── lint.yml │ └── python-publish.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Overview.md └── epistolary-user-flow@8x.png ├── epistolary ├── __init__.py ├── cli │ └── __init__.py ├── document_manager │ ├── __init__.py │ ├── document_manager.py │ ├── filesystem_document_manager.py │ └── remarkable_document_manager.py ├── epiconfig │ └── __init__.py ├── mailbox_manager │ ├── __init__.py │ ├── mailbox_manager.py │ └── smtpimap_mailbox_manager.py ├── orchestrator.py ├── remarkable │ └── __init__.py ├── text_extractor │ ├── __init__.py │ ├── openai_text_extractor.py │ ├── tesseract_text_extractor.py │ └── text_extractor.py └── types.py ├── pyproject.toml └── uv.lock /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Python Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.x 23 | 24 | - name: Install dependencies 25 | run: | 26 | pip install ruff 27 | 28 | - name: Run linting 29 | run: | 30 | ruff check . -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ignore.* 2 | testing_notebooks/ 3 | 4 | # Created by https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks,macos,visualstudiocode 5 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,jupyternotebooks,macos,visualstudiocode 6 | 7 | ### JupyterNotebooks ### 8 | # gitignore template for Jupyter Notebooks 9 | # website: http://jupyter.org/ 10 | 11 | .ipynb_checkpoints 12 | */.ipynb_checkpoints/* 13 | 14 | # IPython 15 | profile_default/ 16 | ipython_config.py 17 | 18 | # Remove previous ipynb_checkpoints 19 | # git rm -r .ipynb_checkpoints/ 20 | 21 | ### macOS ### 22 | # General 23 | .DS_Store 24 | .AppleDouble 25 | .LSOverride 26 | 27 | # Icon must end with two \r 28 | Icon 29 | 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | ### macOS Patch ### 51 | # iCloud generated files 52 | *.icloud 53 | 54 | ### Python ### 55 | # Byte-compiled / optimized / DLL files 56 | __pycache__/ 57 | *.py[cod] 58 | *$py.class 59 | 60 | # C extensions 61 | *.so 62 | 63 | # Distribution / packaging 64 | .Python 65 | build/ 66 | develop-eggs/ 67 | dist/ 68 | downloads/ 69 | eggs/ 70 | .eggs/ 71 | lib/ 72 | lib64/ 73 | parts/ 74 | sdist/ 75 | var/ 76 | wheels/ 77 | share/python-wheels/ 78 | *.egg-info/ 79 | .installed.cfg 80 | *.egg 81 | MANIFEST 82 | 83 | # PyInstaller 84 | # Usually these files are written by a python script from a template 85 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 86 | *.manifest 87 | *.spec 88 | 89 | # Installer logs 90 | pip-log.txt 91 | pip-delete-this-directory.txt 92 | 93 | # Unit test / coverage reports 94 | htmlcov/ 95 | .tox/ 96 | .nox/ 97 | .coverage 98 | .coverage.* 99 | .cache 100 | nosetests.xml 101 | coverage.xml 102 | *.cover 103 | *.py,cover 104 | .hypothesis/ 105 | .pytest_cache/ 106 | cover/ 107 | 108 | # Translations 109 | *.mo 110 | *.pot 111 | 112 | # Django stuff: 113 | *.log 114 | local_settings.py 115 | db.sqlite3 116 | db.sqlite3-journal 117 | 118 | # Flask stuff: 119 | instance/ 120 | .webassets-cache 121 | 122 | # Scrapy stuff: 123 | .scrapy 124 | 125 | # Sphinx documentation 126 | docs/_build/ 127 | 128 | # PyBuilder 129 | .pybuilder/ 130 | target/ 131 | 132 | # Jupyter Notebook 133 | 134 | # IPython 135 | 136 | # pyenv 137 | # For a library or package, you might want to ignore these files since the code is 138 | # intended to run in multiple environments; otherwise, check them in: 139 | # .python-version 140 | 141 | # pipenv 142 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 143 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 144 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 145 | # install all needed dependencies. 146 | #Pipfile.lock 147 | 148 | # poetry 149 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 150 | # This is especially recommended for binary packages to ensure reproducibility, and is more 151 | # commonly ignored for libraries. 152 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 153 | #poetry.lock 154 | 155 | # pdm 156 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 157 | #pdm.lock 158 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 159 | # in version control. 160 | # https://pdm.fming.dev/#use-with-ide 161 | .pdm.toml 162 | 163 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 164 | __pypackages__/ 165 | 166 | # Celery stuff 167 | celerybeat-schedule 168 | celerybeat.pid 169 | 170 | # SageMath parsed files 171 | *.sage.py 172 | 173 | # Environments 174 | .env 175 | .venv 176 | env/ 177 | venv/ 178 | ENV/ 179 | env.bak/ 180 | venv.bak/ 181 | 182 | # Spyder project settings 183 | .spyderproject 184 | .spyproject 185 | 186 | # Rope project settings 187 | .ropeproject 188 | 189 | # mkdocs documentation 190 | /site 191 | 192 | # mypy 193 | .mypy_cache/ 194 | .dmypy.json 195 | dmypy.json 196 | 197 | # Pyre type checker 198 | .pyre/ 199 | 200 | # pytype static type analyzer 201 | .pytype/ 202 | 203 | # Cython debug symbols 204 | cython_debug/ 205 | 206 | # PyCharm 207 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 208 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 209 | # and can be added to the global gitignore or merged into this file. For a more nuclear 210 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 211 | #.idea/ 212 | 213 | ### Python Patch ### 214 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 215 | poetry.toml 216 | 217 | # ruff 218 | .ruff_cache/ 219 | 220 | # LSP config files 221 | pyrightconfig.json 222 | 223 | ### VisualStudioCode ### 224 | .vscode/* 225 | !.vscode/settings.json 226 | !.vscode/tasks.json 227 | !.vscode/launch.json 228 | !.vscode/extensions.json 229 | !.vscode/*.code-snippets 230 | 231 | # Local History for Visual Studio Code 232 | .history/ 233 | 234 | # Built Visual Studio Code Extensions 235 | *.vsix 236 | 237 | ### VisualStudioCode Patch ### 238 | # Ignore all local history of files 239 | .history 240 | .ionide 241 | 242 | # End of https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks,macos,visualstudiocode 243 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.2.0 (September 05, 2024) 4 | 5 | - Features 6 | - Added the command-line `epistolary` tool, which can be used to interact with the package from the command line (#6) 7 | - Housekeeping 8 | - Added GitHub Actions CI/CD for PyPI 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

image

2 | 3 |

PyPI - Version  Static Badge 4 |

5 | 6 | ![Epistolary user flow](docs/epistolary-user-flow@8x.png) 7 | 8 | I find writing emails to be one of the most tedious and unpleasant tasks of my day. But I find the act of handwriting to be one of the most pleasant! The reMarkable is an e-ink tablet with a very pleasant writing experience. This software allows you to respond to emails by writing on the reMarkable, and then sends an OCR'd version of your writing to the recipient. 9 | 10 | --- 11 | 12 | See [docs/Overview.md](docs/Overview.md) to see an example in action! 13 | 14 | --- 15 | 16 | This tool is designed to "print" emails to a PDF file (one thread per file), with a blank (ruled) page after each email. 17 | You can write a reply to the email on the blank page, and Epistolary will convert your handwriting to text and send it as a reply to the email. 18 | 19 | You can also write a new email by creating a file called `recipient@example.com:::My cool subject`, writing a letter, and then moving it to the `Outbox` folder. 20 | 21 | It is designed to be used with the [ReMarkable](https://remarkable.com/) tablet, which is a great device for reading and annotating PDFs, but it should work with standalone PDFs, tablet devices, or scanned documents as well. 22 | 23 | ## Architecture 24 | 25 | The tool comprises three main components: 26 | 27 | - `MailboxManager`: A class that manages the mailbox, and provides methods to get the next email to be printed, and to send a reply to an email. 28 | - `DocumentManager`: A class that manages the PDF document library. 29 | - `EpistolaryOrchestrator`: A class that manages the interaction between the `MailboxManager` and the `DocumentManager`, and provides OCR and main entry point functionality. 30 | 31 | ## Installation 32 | 33 | ### 0. Requirements 34 | 35 | If you are using the tesseract OCR option, you must first install Tesseract for OCR. On MacOS, this can be done with `brew install tesseract`. 36 | 37 | If you are using the OpenAI OCR option, you should configure your OpenAI API key globally, or you can pass it in a config to the TextExtractor directly. 38 | 39 | If you are planning to use the reMarkable document management tools, you will also need to install [the `rmapi` tool](https://github.com/juruen/rmapi) and configure it with your reMarkable account. 40 | 41 | ### 1. Install the package 42 | 43 | Optionally, install Python dependencies: 44 | 45 | ```bash 46 | uv install 47 | ``` 48 | 49 | (If you do not do this explicitly, the package will install the dependencies automatically when you run the package.) 50 | 51 | ### 2. Configure the package 52 | 53 | This can be done in one of two ways: Manually, editing a config file; or by running the `init` wizard. 54 | 55 | We STRONGLY recommend using the `init` wizard, as it will guide you through the process of setting up the config file. 56 | 57 | #### Option 1: Run the `init` wizard 58 | 59 | ```bash 60 | uv run epistolary init 61 | ``` 62 | 63 | This will guide you through the process of setting up the config file, passing in email credentials and the configurations for the document managers you'd like to use. You can also specify the location of a config file by passing the `--config`/`-c` flag: 64 | 65 | ```bash 66 | uv run epistolary --config ~/.config/epistolary.json init 67 | ``` 68 | 69 | #### Option 2: Manually edit the config file 70 | 71 | Create a `~/.config/epistolary.json` file. Here's an example of what it might look like: 72 | 73 | ```jsonc 74 | { 75 | "email": "####", 76 | "password": "####", 77 | "imap": { 78 | "host": "####", 79 | "port": 993 80 | }, 81 | "smtp": { 82 | "host": "####", 83 | "port": 587 84 | }, 85 | 86 | // Optional (defaults shown) 87 | 88 | "smtp_username": null, 89 | "smtp_password": null, 90 | 91 | "ignore_marketing_emails": true, 92 | "document_manager": "epistolary.document_manager.remarkable_document_manager:RemarkableDocumentManager", 93 | "text_extractor": "epistolary.text_extractor.openai_text_extractor:OpenAITextExtractor" 94 | } 95 | ``` 96 | 97 | ## Usage 98 | 99 | We recommend doing this with the `epistolary` cli: 100 | 101 | ```bash 102 | # Get new emails and "print" them to your device: 103 | uv run epistolary receive 104 | ``` 105 | 106 | ```bash 107 | # Send the replies you've written: 108 | uv run epistolary send 109 | ``` 110 | 111 | Alternatively, here's an example of a Python script that does the same thing as the above commands: 112 | 113 | ```python 114 | from epistolary.orchestrator import EpistolaryOrchestrator 115 | from epistolary.mailbox_manager import SMTPIMAPMailboxManager 116 | from epistolary.document_manager.remarkable_document_manager import RemarkableDocumentManager 117 | from epistolary.text_extractor.openai_text_extractor import OpenAITextExtractor 118 | 119 | EO = EpistolaryOrchestrator( 120 | SMTPIMAPMailboxManager.from_file(), 121 | RemarkableDocumentManager(), 122 | text_extractor=OpenAITextExtractor(), 123 | debug=True, 124 | ) 125 | 126 | ``` 127 | 128 | To receive emails: 129 | 130 | ```python 131 | EO.refresh_document_mailbox() 132 | ``` 133 | 134 | And to send: 135 | 136 | ```python 137 | EO.send_outbox() 138 | ``` 139 | 140 | Note that the `SMTPIMAPMailboxManager` uses the `epistolary.json` file to get the email and password, and the `RemarkableDocumentManager` uses the `rmapi` tool to manage the reMarkable documents (which depends upon a `~/.rmapi` file with your reMarkable credentials). 141 | 142 | If you do not choose to use the `#from_file()` method, you can pass in the email, password, and other parameters directly to the `SMTPIMAPMailboxManager` constructor. 143 | 144 | ## Known Limitations 145 | 146 | - No support for inline images or attachments yet... But it should be easy to add! 147 | - Spacing and formatting of the OCR'd text is not perfect, but it's usually good enough for a quick reply. 148 | -------------------------------------------------------------------------------- /docs/Overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Suppose someone writes you an email. It shows up in your linked inbox like this: 4 | 5 | 6 | 7 | image 8 | 9 | While it's still unread, the Epistolary library will download it and convert it to a PDF. Depending on your document-manager, it will be uploaded to a destination of your choice. In this case, it's uploaded to my reMarkable tablet: 10 | 11 | ![image](https://github.com/user-attachments/assets/4d8cefb1-a57d-443a-878c-5bf9d4b664af) 12 | 13 | image 14 | 15 | You can then write a reply in handwriting: 16 | 17 | ![image](https://github.com/user-attachments/assets/4c6ef9f0-3d31-4c87-9faf-1d7484901bc9) 18 | 19 | Once you're done, move the file to the Outbox to mark it ready to be posted. Epistolary will then transcribe the text and send the email back to the originating address as a Reply. 20 | 21 | Here's how it appears to the recipient: 22 | 23 | --- 24 | 25 | ![image](https://github.com/user-attachments/assets/fc7e5c75-ddad-4d73-ab3e-56d5f5c2725f) 26 | -------------------------------------------------------------------------------- /docs/epistolary-user-flow@8x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j6k4m8/epistolary/67e48765d76541108aec3ee144b943e2a60c0f30/docs/epistolary-user-flow@8x.png -------------------------------------------------------------------------------- /epistolary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j6k4m8/epistolary/67e48765d76541108aec3ee144b943e2a60c0f30/epistolary/__init__.py -------------------------------------------------------------------------------- /epistolary/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import pathlib 3 | 4 | import click 5 | 6 | from ..epiconfig import ( 7 | _DEFAULT_DOCUMENT_MANAGER, 8 | _DEFAULT_TEXT_EXTRACTOR, 9 | _DOCUMENT_MANAGER_OPTIONS, 10 | _TEXT_EXTRACTOR_OPTIONS, 11 | EpistolaryConfig, 12 | _get_module_from_string, 13 | ) 14 | from ..mailbox_manager.smtpimap_mailbox_manager import SMTPIMAPMailboxManager 15 | from ..orchestrator import EpistolaryOrchestrator 16 | 17 | 18 | @click.group() 19 | @click.option( 20 | "--config", 21 | "-c", 22 | default="~/.config/epistolary.json", 23 | help="Path to the config file.", 24 | ) 25 | @click.pass_context 26 | def cli(ctx, config): 27 | ctx.ensure_object(dict) 28 | ctx.obj["config_file_path"] = pathlib.Path(config).expanduser() 29 | 30 | 31 | @cli.command() 32 | @click.pass_context 33 | def init(ctx): 34 | # First we need to check if the config file exists. 35 | config_path = ctx.obj["config_file_path"] 36 | if config_path.exists(): 37 | click.echo( 38 | click.style(f"Config file already exists at {config_path}", fg="red"), 39 | err=True, 40 | ) 41 | return 42 | 43 | # If the config file does not exist, we can now prompt the user for the 44 | # necessary information. 45 | click.echo(f"Creating a new Epistolary config in {config_path}.") 46 | email = click.prompt("Email") 47 | password = base64.b64encode( 48 | click.prompt("Password", hide_input=True).encode("utf-8") 49 | ).decode("utf-8") 50 | imap_host = click.prompt("IMAP Host") 51 | imap_port = click.prompt("IMAP Port", type=int) 52 | smtp_host = click.prompt("SMTP Host") 53 | smtp_port = click.prompt("SMTP Port", type=int) 54 | 55 | # Do we have a separate SMTP username and password y/N? 56 | smtp_username = None 57 | smtp_password = None 58 | if click.confirm( 59 | "Do you have a separate SMTP username and password?", default=False 60 | ): 61 | smtp_username = click.prompt("SMTP Username") 62 | smtp_password = click.prompt("SMTP Password", hide_input=True) 63 | 64 | # Should we ignore marketing emails y/N? 65 | click.echo( 66 | "By default, Epistolary will ignore marketing emails, defined as any email that has the text 'unsubscribe' in the body." 67 | ) 68 | ignore_marketing_emails = not click.confirm( 69 | "Would you like to forward marketing emails to your device (true) or ignore them (false, default)?", 70 | default=False, 71 | ) 72 | 73 | # We can now prompt for which extractor, doc mgr, and mailbox classes to 74 | # use, as a "choice": 75 | 76 | # Extractor 77 | click.echo( 78 | "Epistolary can use different extractors to extract the text from emails." 79 | ) 80 | click.echo("\t- 'tesseract': Extract text using the Tesseract OCR engine.") 81 | click.echo( 82 | "\t- 'openai': Extract text using the OpenAI GPT-4o engine. NOTE: This transmits your email as an image to OpenAI servers." 83 | ) 84 | extractor = click.prompt( 85 | "Which extractor would you like to use?", 86 | type=click.Choice(list(_TEXT_EXTRACTOR_OPTIONS.keys())), 87 | default=_DEFAULT_TEXT_EXTRACTOR, 88 | ) 89 | 90 | try: 91 | _ = _get_module_from_string(_TEXT_EXTRACTOR_OPTIONS[extractor]) 92 | except Exception as e: 93 | click.echo(click.style(f"Failed to load extractor: {e}", fg="red"), err=True) 94 | return 95 | 96 | # Document Manager 97 | click.echo("Epistolary can use different document managers to manage the emails.") 98 | click.echo("\t- 'files': Save email PDFs to the filesystem.") 99 | click.echo("\t- 'remarkable': Interact with emails on a reMarkable tablet.") 100 | document_manager = click.prompt( 101 | "Which document manager would you like to use?", 102 | type=click.Choice(list(_DOCUMENT_MANAGER_OPTIONS.keys())), 103 | default=_DEFAULT_DOCUMENT_MANAGER, 104 | ) 105 | 106 | try: 107 | _ = _get_module_from_string(_DOCUMENT_MANAGER_OPTIONS[document_manager]) 108 | except Exception as e: 109 | click.echo( 110 | click.style(f"Failed to load document manager: {e}", fg="red"), err=True 111 | ) 112 | return 113 | 114 | # Now we can create the config object. 115 | conf = EpistolaryConfig( 116 | imap_host=imap_host, 117 | imap_port=imap_port, 118 | email=email, 119 | password=password, 120 | smtp_host=smtp_host, 121 | smtp_port=smtp_port, 122 | smtp_username=smtp_username, 123 | smtp_password=smtp_password, 124 | ignore_marketing_emails=ignore_marketing_emails, 125 | text_extractor=_TEXT_EXTRACTOR_OPTIONS[extractor], 126 | document_manager=_DOCUMENT_MANAGER_OPTIONS[document_manager], 127 | ) 128 | 129 | click.echo( 130 | "Epistolary can optionally check that your email credentials are correct by performing a test login." 131 | ) 132 | 133 | test_login = click.confirm("Would you like to perform a test login?", default=True) 134 | if test_login: 135 | click.echo("Attempting to log in...") 136 | try: 137 | _ = SMTPIMAPMailboxManager( 138 | imap_host=conf.imap_host, 139 | imap_port=conf.imap_port, 140 | username=conf.email, 141 | password=conf.password, 142 | smtp_host=conf.smtp_host, 143 | smtp_port=conf.smtp_port, 144 | smtp_username=conf.smtp_username, 145 | smtp_password=conf.smtp_password, 146 | ) 147 | except Exception as e: 148 | click.echo(click.style(f"Failed to log in: {e}", fg="red"), err=True) 149 | return 150 | click.echo(click.style("Successfully logged in!", fg="green")) 151 | 152 | # Finally, we can write the config to the file. 153 | conf.to_file(config_path) 154 | click.echo(click.style(f"Config successfully written to {config_path}", fg="green")) 155 | 156 | 157 | @cli.command() 158 | @click.pass_context 159 | def receive(ctx): 160 | config_path = ctx.obj["config_file_path"] 161 | if not config_path.exists(): 162 | click.echo( 163 | click.style(f"Config file does not exist at {config_path}", fg="red"), 164 | err=True, 165 | ) 166 | return 167 | 168 | config = EpistolaryConfig.from_file(config_path) 169 | 170 | EO = EpistolaryOrchestrator( 171 | mailbox_manager=SMTPIMAPMailboxManager.from_file(config_path), 172 | document_manager=_get_module_from_string(config.document_manager)(), 173 | text_extractor=_get_module_from_string(config.text_extractor)(), 174 | ) 175 | 176 | EO.refresh_document_mailbox() 177 | 178 | 179 | @cli.command() 180 | @click.pass_context 181 | def send(ctx): 182 | config_path = ctx.obj["config_file_path"] 183 | if not config_path.exists(): 184 | click.echo( 185 | click.style(f"Config file does not exist at {config_path}", fg="red"), 186 | err=True, 187 | ) 188 | return 189 | 190 | config = EpistolaryConfig.from_file(config_path) 191 | 192 | EO = EpistolaryOrchestrator( 193 | mailbox_manager=SMTPIMAPMailboxManager.from_file(config_path), 194 | document_manager=_get_module_from_string(config.document_manager)(), 195 | text_extractor=_get_module_from_string(config.text_extractor)(), 196 | ) 197 | 198 | EO.send_outbox() 199 | 200 | 201 | if __name__ == "__main__": 202 | cli() 203 | -------------------------------------------------------------------------------- /epistolary/document_manager/__init__.py: -------------------------------------------------------------------------------- 1 | from .document_manager import DocumentManager 2 | from .filesystem_document_manager import FilesystemDocumentManager 3 | 4 | 5 | __all__ = ["DocumentManager", "FilesystemDocumentManager"] 6 | -------------------------------------------------------------------------------- /epistolary/document_manager/document_manager.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | from fitz import Document 3 | from ..types import DocumentID 4 | 5 | 6 | class DocumentManager(Protocol): 7 | """A protocol for classes that manage documents.""" 8 | 9 | def get_documents(self) -> dict[DocumentID, Document]: 10 | """Return a list of documents. 11 | 12 | Returns: 13 | A dictionary of documents, with the document ID as the key and the 14 | document as the value. 15 | 16 | """ 17 | ... 18 | 19 | def get_edited_documents(self) -> dict[DocumentID, Document]: 20 | """Return a list of edited documents. 21 | 22 | Returns: 23 | A dictionary of documents, with the document ID as the key and the 24 | document as the value. 25 | 26 | """ 27 | ... 28 | 29 | def list_documents(self) -> list[DocumentID]: 30 | """Return a list of document IDs. 31 | 32 | Returns: 33 | A list of document IDs. 34 | 35 | """ 36 | ... 37 | 38 | def get_document(self, uid: DocumentID) -> Document: 39 | """Get a single document. 40 | 41 | Arguments: 42 | uid: The ID of the document to get. 43 | 44 | Returns: 45 | The requested document. 46 | 47 | """ 48 | ... 49 | 50 | def has_document(self, uid: DocumentID) -> bool: 51 | """Check if a document exists. 52 | 53 | Arguments: 54 | uid: The ID of the document to check. 55 | 56 | Returns: 57 | True if the document exists, False otherwise. 58 | 59 | """ 60 | ... 61 | 62 | def append_ruled_page_to_document(self, document: Document) -> Document: 63 | """Append a ruled page to a document. 64 | 65 | Does not modify the original document. 66 | 67 | Arguments: 68 | document: The document to append the page to. 69 | 70 | Returns: 71 | A NEW document with the page appended. 72 | 73 | """ 74 | ... 75 | 76 | def put_document( 77 | self, document: Document, requested_document_id: DocumentID 78 | ) -> DocumentID: 79 | """Put a document into the document manager. 80 | 81 | Arguments: 82 | document: The document to put. 83 | requested_document_id: The ID to use for the document. 84 | 85 | Returns: 86 | The ID of the document. 87 | 88 | """ 89 | ... 90 | 91 | def delete_document(self, uid: DocumentID) -> bool: 92 | """Delete a document. 93 | 94 | Arguments: 95 | uid: The ID of the document to delete. 96 | 97 | Returns: 98 | True if the document was deleted successfully, False otherwise. 99 | 100 | """ 101 | ... 102 | -------------------------------------------------------------------------------- /epistolary/document_manager/filesystem_document_manager.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import fitz 3 | from .document_manager import DocumentManager 4 | 5 | from epistolary.types import DocumentID 6 | 7 | 8 | class FilesystemDocumentManager(DocumentManager): 9 | """A DocumentManager that stores documents in a local filesystem. 10 | 11 | Each document is a PDF, and they are lazily loaded with pypdf when the 12 | user requests them. 13 | """ 14 | 15 | def __init__(self, root_path: pathlib.Path): 16 | self.root_path = root_path 17 | 18 | def get_documents(self) -> dict[DocumentID, fitz.Document]: 19 | """Return a list of documents. 20 | 21 | Returns: 22 | A dictionary of documents, with the document ID as the key and the 23 | document as the value. 24 | 25 | """ 26 | return { 27 | DocumentID(path.stem): fitz.open(path) # type: ignore 28 | for path in self.root_path.glob("*.pdf") 29 | } 30 | 31 | def list_documents(self) -> list[DocumentID]: 32 | """Return a list of document IDs. 33 | 34 | Returns: 35 | A list of document IDs. 36 | 37 | """ 38 | return [DocumentID(path.stem) for path in self.root_path.glob("*.pdf")] 39 | 40 | def get_document(self, document_id: DocumentID) -> fitz.Document: 41 | """Return a single document. 42 | 43 | Args: 44 | document_id: The ID of the document to return. 45 | 46 | Returns: 47 | The document with the given ID. 48 | 49 | """ 50 | return fitz.open(self.root_path / f"{document_id}.pdf") # type: ignore 51 | 52 | def has_document(self, document_id: DocumentID) -> bool: 53 | """Return whether a document exists. 54 | 55 | Args: 56 | document_id: The ID of the document to check for. 57 | 58 | Returns: 59 | Whether the document exists. 60 | 61 | """ 62 | return (self.root_path / f"{document_id}.pdf").exists() 63 | 64 | def append_ruled_page_to_document(self, document: fitz.Document) -> fitz.Document: 65 | """Append a ruled page to a document. 66 | 67 | Arguments: 68 | document: The document to append the page to. 69 | 70 | Returns: 71 | A NEW document with the page appended. 72 | 73 | """ 74 | # TODO: For now, just append a blank page 75 | _page = document.new_page(-1) # type: ignore 76 | return document 77 | 78 | def put_document( 79 | self, document: fitz.Document, requested_document_id: DocumentID 80 | ) -> DocumentID: 81 | """Put a document into the document manager. 82 | 83 | Arguments: 84 | document: The document to put. 85 | requested_document_id: The ID to use for the document. 86 | 87 | Returns: 88 | The ID of the document that was put. 89 | 90 | """ 91 | if self.has_document(requested_document_id): 92 | raise ValueError("Document already exists!") 93 | document.save(self.root_path / f"{requested_document_id}.pdf") 94 | return requested_document_id 95 | 96 | def delete_document(self, document_id: DocumentID) -> None: 97 | """Delete a document. 98 | 99 | Args: 100 | document_id: The ID of the document to delete. 101 | 102 | """ 103 | (self.root_path / f"{document_id}.pdf").unlink() 104 | -------------------------------------------------------------------------------- /epistolary/document_manager/remarkable_document_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | The reMarkable paper tablet has a long and storied hacker-facing API history. 3 | 4 | The short version is that there is no reliable way to interact with the 5 | reMarkable tablet filesystem with Python directly. Instead the current standard 6 | way to talk to the tablet is with the Go rmapi tool, which is no longer main- 7 | tained because the reMarkable company has continued to move the goalposts on 8 | their API. Oh well! 9 | 10 | Annoyingly they have also changed the standard format for the documents stored 11 | on the tablet, so even though the "actual" format is a PDF, the files come off 12 | of the tablet in an undocumented directory tree that includes several files per 13 | document. The best way I've found to interact with this is through a fork of 14 | the `remarks` tool, which, although Python, is not really intended for use as a 15 | Python library. 16 | 17 | So this class unfortunately serves more as a coordinator between the reMarkable 18 | API and the rest of the epistolary codebase, rather than a true DocumentManager 19 | implementation. 20 | """ 21 | 22 | import pathlib 23 | import tempfile 24 | import fitz 25 | from .document_manager import DocumentManager 26 | 27 | from ..types import DocumentID 28 | from ..remarkable import RemarksWrapper, RMAPIWrapper, ReMarkablePathType 29 | 30 | 31 | class RemarkableDocumentManager(DocumentManager): 32 | """A DocumentManager that stores documents on a reMarkable tablet.""" 33 | 34 | _local_cache_root_path: pathlib.Path 35 | 36 | def __init__( 37 | self, 38 | local_cache_root_path: pathlib.Path | None = None, 39 | remarkable_root_path: str = "Emails", 40 | remarkable_outbox_subdirectory: str = "Outbox", 41 | debug: bool = False, 42 | ): 43 | """ 44 | Create a new RemarkableDocumentManager. 45 | 46 | Arguments: 47 | local_cache_root_path: The path to the local cache directory where 48 | the document manager. Defaults to None, in which case a new 49 | temp directory is created and used. 50 | remarkable_root_path: The path to the root directory on the tablet 51 | where the documents are stored. Defaults to "Emails". 52 | remarkable_outbox_subdirectory: The subdirectory of the root 53 | directory where the outbox is stored. Defaults to "Outbox". 54 | debug: Whether to print debug information. Defaults to False. 55 | """ 56 | self._local_cache_root_path = local_cache_root_path # type: ignore 57 | self._provision_local_cache() 58 | 59 | self._rmapi = RMAPIWrapper(cache_dir=self._local_cache_root_path) 60 | self._remarkable_root_path = remarkable_root_path 61 | self._remarkable_outbox_subdirectory = remarkable_outbox_subdirectory 62 | self._provision_remarkable_directory() 63 | self._remarks = RemarksWrapper(debug=debug) 64 | self._debug = debug 65 | 66 | def _provision_local_cache(self): 67 | """Provision the local cache directory.""" 68 | if self._local_cache_root_path is None: 69 | self._local_cache_root_path = pathlib.Path(tempfile.mkdtemp()) 70 | self._local_cache_root_path.mkdir(parents=True, exist_ok=True) 71 | 72 | def _provision_remarkable_directory(self): 73 | """Provision the reMarkable directory.""" 74 | # Check if the directory exists 75 | self._rmapi.mkdir(self._remarkable_root_path) 76 | self._rmapi.mkdir( 77 | self._remarkable_root_path + "/" + self._remarkable_outbox_subdirectory 78 | ) 79 | 80 | def get_documents(self) -> dict[DocumentID, fitz.Document]: 81 | """Return a list of documents. 82 | 83 | This function works by performing two steps in sequence: 84 | 85 | It first reads from the tablet (using rmapi) and mirrors the files to 86 | the local cache. Then it converts each document in the local cache to a 87 | fitz.Document object by first converting the document to a PDF with the 88 | `remarks` tool and then opening the PDF with fitz. 89 | 90 | Note that this makes this function very slow, and it should only be 91 | used if you _actually_ need all of the documents. Instead, we recommend 92 | using the `get_document` function to get a single document, or the 93 | `list_documents` function to get a list of document IDs. 94 | 95 | Returns: 96 | A dictionary of documents, with the document ID as the key and the 97 | document as the value. 98 | 99 | """ 100 | document_ids = self.list_documents() 101 | documents = {} 102 | for document_id in document_ids: 103 | documents[document_id] = self.get_document(document_id) 104 | return documents 105 | 106 | def get_edited_documents(self) -> dict[DocumentID, fitz.Document]: 107 | """Return a list of edited documents. 108 | 109 | Returns: 110 | A dictionary of documents, with the document ID as the key and the 111 | document as the value. 112 | 113 | """ 114 | # Get all docs from the outbox 115 | files = self._rmapi.ls( 116 | self._remarkable_root_path + "/" + self._remarkable_outbox_subdirectory 117 | ) 118 | return { 119 | document_id: self.get_document( 120 | f"{self._remarkable_outbox_subdirectory}/{document_id}" 121 | ) 122 | for document_type, document_id in files 123 | if document_type == ReMarkablePathType.FILE 124 | } 125 | 126 | def list_documents(self) -> list[DocumentID]: 127 | """Return a list of document IDs. 128 | 129 | Returns: 130 | A list of document IDs. 131 | 132 | """ 133 | files = self._rmapi.ls(self._remarkable_root_path) 134 | return [ 135 | path for path_type, path in files if path_type == ReMarkablePathType.FILE 136 | ] 137 | 138 | def get_document(self, uid: DocumentID) -> fitz.Document: 139 | """Get a single document. 140 | 141 | This is the function that actually downloads the document from the 142 | reMarkable cloud using rmapi, converts it to a PDF using the `remarks` 143 | tool, and then opens the PDF with fitz. 144 | 145 | Arguments: 146 | uid: The ID of the document to get. 147 | 148 | Returns: 149 | The requested document. 150 | 151 | """ 152 | abs_path = self._local_cache_root_path / uid 153 | self._rmapi.download(self._remarkable_root_path + "/" + uid, abs_path) 154 | zip_path = pathlib.Path(str(abs_path) + ".zip") 155 | pdf_path = pathlib.Path(str(abs_path) + ".pdf") 156 | self._remarks.rm_to_pdf(zip_path, pdf_path) 157 | if self._debug: 158 | print(zip_path, pdf_path) 159 | return fitz.open(pdf_path) # type: ignore 160 | 161 | def has_document(self, uid: DocumentID) -> bool: 162 | """Check if a document exists. 163 | 164 | Arguments: 165 | uid: The ID of the document to check for. 166 | 167 | Returns: 168 | True if the document exists, False otherwise. 169 | 170 | """ 171 | return (uid + ".pdf") in self.list_documents() 172 | 173 | def put_document(self, document: fitz.Document, uid: DocumentID): 174 | """Upload a document to the reMarkable. 175 | 176 | Arguments: 177 | document: The document to upload. 178 | uid: The ID of the document. 179 | 180 | """ 181 | pdf_path = pathlib.Path(str(self._local_cache_root_path / uid) + ".pdf") 182 | document.save(pdf_path) 183 | self._rmapi.upload(pdf_path, self._remarkable_root_path) 184 | 185 | def delete_document(self, uid: DocumentID) -> bool: 186 | # Create a `_Archive` directory if it doesn't exist, inside the root: 187 | self._rmapi.mkdir(self._remarkable_root_path + "/_Archive") 188 | # Move the document to the `_Archive` directory: 189 | try: 190 | self._rmapi.move( 191 | f"{self._remarkable_root_path}/{uid}", 192 | f"{self._remarkable_root_path}/_Archive", 193 | ) 194 | except Exception as _e: 195 | try: 196 | self._rmapi.move( 197 | f"{self._remarkable_root_path}/{self._remarkable_outbox_subdirectory}/{uid}", 198 | f"{self._remarkable_root_path}/_Archive", 199 | ) 200 | except Exception as _e: 201 | return False 202 | return True 203 | 204 | def append_ruled_page_to_document(self, document: fitz.Document) -> fitz.Document: 205 | _page = document.new_page(-1) # type: ignore 206 | return document 207 | -------------------------------------------------------------------------------- /epistolary/epiconfig/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import pathlib 4 | from typing import Optional 5 | import importlib 6 | 7 | 8 | def _get_module_from_string(module_str): 9 | # The string should be in the format of 'module.submodule:class' 10 | module_str, class_str = module_str.split(":") 11 | module = importlib.import_module(module_str) 12 | return getattr(module, class_str) 13 | 14 | 15 | # Schema: 16 | _CONFIG_SCHEMA = { 17 | "imap": { 18 | "host": str, 19 | "port": int, 20 | }, 21 | "email": str, 22 | "password": str, 23 | "smtp": { 24 | "host": str, 25 | "port": int, 26 | }, 27 | "smtp_username": Optional[str], 28 | "smtp_password": Optional[str], 29 | "ignore_marketing_emails": Optional[bool], 30 | "mailbox_manager": str, 31 | "text_extractor": str, 32 | "document_manager": str, 33 | } 34 | 35 | _MAILBOX_MANAGER_OPTIONS = { 36 | "default": "epistolary.mailbox_manager.smtpimap_mailbox_manager:SMTPIMAPMailboxManager", 37 | } 38 | _DEFAULT_MAILBOX_MANAGER = "default" 39 | 40 | 41 | _TEXT_EXTRACTOR_OPTIONS = { 42 | "openai": "epistolary.text_extractor.openai_text_extractor:OpenAITextExtractor", 43 | "tesseract": "epistolary.text_extractor.tesseract_text_extractor:TesseractTextExtractor", 44 | } 45 | _DEFAULT_TEXT_EXTRACTOR = "openai" 46 | 47 | _DOCUMENT_MANAGER_OPTIONS = { 48 | "files": "epistolary.document_manager.filesystem_document_manager:FilesystemDocumentManager", 49 | "remarkable": "epistolary.document_manager.remarkable_document_manager:RemarkableDocumentManager", 50 | } 51 | _DEFAULT_DOCUMENT_MANAGER = "remarkable" 52 | 53 | 54 | class EpistolaryConfig: 55 | """The configuration for Epistolary.""" 56 | 57 | def __init__( 58 | self, 59 | imap_host: str, 60 | imap_port: int, 61 | email: str, 62 | password: str, 63 | smtp_host: str, 64 | smtp_port: int, 65 | smtp_username: Optional[str] = None, 66 | smtp_password: Optional[str] = None, 67 | ignore_marketing_emails: Optional[bool] = True, 68 | document_manager: str = _DEFAULT_DOCUMENT_MANAGER, 69 | text_extractor: str = _DEFAULT_TEXT_EXTRACTOR, 70 | ): 71 | """Create a new EpistolaryConfig object. 72 | 73 | Arguments: 74 | imap_host (str): The hostname of the IMAP server. 75 | imap_port (int): The port number of the IMAP server. 76 | email (str): The email address. 77 | password (str): The password. 78 | smtp_host (str): The hostname of the SMTP server. 79 | smtp_port (int): The port number of the SMTP server. 80 | smtp_username (str, optional): The username for the SMTP server. 81 | Defaults to None. 82 | smtp_password (str, optional): The password for the SMTP server. 83 | Defaults to None. 84 | ignore_marketing_emails (bool, optional): Whether to ignore 85 | marketing emails. Defaults to True. 86 | document_manager (str, optional): The document manager to use. 87 | Defaults to _DEFAULT_DOCUMENT_MANAGER. 88 | text_extractor (str, optional): The text extractor to use. 89 | Defaults to _DEFAULT_TEXT_EXTRACTOR. 90 | 91 | """ 92 | self.imap_host = imap_host 93 | self.imap_port = imap_port 94 | self.email = email 95 | self._password = password 96 | self.smtp_host = smtp_host 97 | self.smtp_port = smtp_port 98 | self.smtp_username = smtp_username 99 | self.smtp_password = smtp_password 100 | self.ignore_marketing_emails = ignore_marketing_emails 101 | self.document_manager = document_manager 102 | self.text_extractor = text_extractor 103 | 104 | @property 105 | def password(self) -> str: 106 | """Get the decoded password.""" 107 | return base64.b64decode(self._password.encode("utf-8")).decode("utf-8") 108 | 109 | @classmethod 110 | def from_dict(cls, config: dict) -> "EpistolaryConfig": 111 | """Create a new EpistolaryConfig object from a dictionary. 112 | 113 | Arguments: 114 | config (dict): The configuration dictionary. 115 | 116 | Returns: 117 | EpistolaryConfig: The configuration object. 118 | 119 | """ 120 | return cls( 121 | imap_host=config["imap"]["host"], 122 | imap_port=config["imap"]["port"], 123 | email=config["email"], 124 | password=config["password"], 125 | smtp_host=config["smtp"]["host"], 126 | smtp_port=config["smtp"]["port"], 127 | smtp_username=config.get("smtp_username"), 128 | smtp_password=config.get("smtp_password"), 129 | ignore_marketing_emails=config.get("ignore_marketing_emails", True), 130 | document_manager=config.get("document_manager", _DEFAULT_DOCUMENT_MANAGER), 131 | text_extractor=config.get("text_extractor", _DEFAULT_TEXT_EXTRACTOR), 132 | ) 133 | 134 | @classmethod 135 | def from_file( 136 | cls, path: str | pathlib.Path = "~/.config/epistolary.json" 137 | ) -> "EpistolaryConfig": 138 | """Create a new EpistolaryConfig object from a file. 139 | 140 | Arguments: 141 | path (str | pathlib.Path, optional): The path to the file. 142 | Defaults to "~/.config/epistolary.json". 143 | 144 | Returns: 145 | EpistolaryConfig: The configuration object. 146 | 147 | """ 148 | path = pathlib.Path(path).expanduser().resolve() 149 | with open(path) as f: 150 | config = json.load(f) 151 | return cls.from_dict(config) 152 | 153 | def to_dict(self) -> dict: 154 | """Convert the configuration object to a dictionary. 155 | 156 | Returns: 157 | dict: The configuration dictionary. 158 | 159 | """ 160 | return { 161 | "imap": { 162 | "host": self.imap_host, 163 | "port": self.imap_port, 164 | }, 165 | "email": self.email, 166 | "password": self._password, 167 | "smtp": { 168 | "host": self.smtp_host, 169 | "port": self.smtp_port, 170 | }, 171 | "smtp_username": self.smtp_username, 172 | "smtp_password": self.smtp_password, 173 | "ignore_marketing_emails": self.ignore_marketing_emails, 174 | "document_manager": self.document_manager, 175 | "text_extractor": self.text_extractor, 176 | } 177 | 178 | def to_file(self, path: str | pathlib.Path = "~/.config/epistolary.json") -> None: 179 | """Write the configuration object to a file. 180 | 181 | Arguments: 182 | path (str | pathlib.Path, optional): The path to the file. 183 | Defaults to "~/.config/epistolary.json". 184 | 185 | """ 186 | path = pathlib.Path(path).expanduser().resolve() 187 | with open(path, "w") as f: 188 | json.dump(self.to_dict(), f) 189 | -------------------------------------------------------------------------------- /epistolary/mailbox_manager/__init__.py: -------------------------------------------------------------------------------- 1 | from .mailbox_manager import MailboxManager 2 | from .smtpimap_mailbox_manager import SMTPIMAPMailboxManager 3 | 4 | __all__ = ["MailboxManager", "SMTPIMAPMailboxManager"] 5 | -------------------------------------------------------------------------------- /epistolary/mailbox_manager/mailbox_manager.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | from redbox.models import EmailMessage 3 | from ..types import EmailID 4 | 5 | 6 | class MailboxManager(Protocol): 7 | """A protocol for classes that manage mailboxes.""" 8 | 9 | def get_emails( 10 | self, folder: str | None = None, limit: int | None = None 11 | ) -> dict[EmailID, EmailMessage]: 12 | """Return a list of emails in the specified folder. 13 | 14 | Arguments: 15 | folder: The folder to get emails from. If None, get emails from 16 | all folders. 17 | limit: The maximum number of emails to return. If None, return all 18 | 19 | Returns: 20 | A dictionary of emails, with the email ID as the key and the email 21 | as the value. 22 | """ 23 | ... 24 | 25 | def get_email(self, uid: EmailID) -> EmailMessage: 26 | """Get a single email. 27 | 28 | Arguments: 29 | uid: The ID of the email to get. 30 | 31 | Returns: 32 | The requested email. 33 | 34 | """ 35 | ... 36 | 37 | def get_email_address_and_subject(self, uid: EmailID) -> tuple[str, str]: 38 | """Get the email address and subject of an email. 39 | 40 | Arguments: 41 | uid: The ID of the email to get. 42 | 43 | Returns: 44 | The email address and subject of the email. 45 | 46 | """ 47 | ... 48 | 49 | def get_email_subject_and_text(self, uid: EmailID) -> tuple[str, str]: 50 | """Get the subject and text of an email. 51 | 52 | Arguments: 53 | uid: The ID of the email to get. 54 | 55 | Returns: 56 | The subject and text of the email. 57 | 58 | """ 59 | ... 60 | 61 | def send_message( 62 | self, to: str, subject: str, body: str, in_reply_to: EmailID | None = None 63 | ) -> bool: 64 | """ 65 | Send a message to the specified recipient. 66 | 67 | Argumentss: 68 | to: The email address of the recipient. 69 | subject: The subject of the email. 70 | body: The body of the email. 71 | in_reply_to: The ID of the email to reply to, if any. 72 | 73 | Returns: 74 | True if the message was sent successfully, False otherwise. 75 | 76 | """ 77 | ... 78 | -------------------------------------------------------------------------------- /epistolary/mailbox_manager/smtpimap_mailbox_manager.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from urllib.parse import quote 3 | from urllib.parse import unquote 4 | from redbox import EmailBox 5 | from redbox.models import EmailMessage 6 | from redmail.email.sender import EmailSender 7 | 8 | from ..epiconfig import EpistolaryConfig 9 | from ..types import EmailID 10 | from .mailbox_manager import MailboxManager 11 | 12 | 13 | def sanitize_fname(fname: str) -> str: 14 | return quote(fname, safe="-_.@") 15 | 16 | 17 | def desanitize_fname(fname: str) -> str: 18 | return unquote(fname) 19 | 20 | 21 | def _get_mail_filename(header_dict: dict[str, str]) -> EmailID: 22 | sender = sanitize_fname(header_dict.get("From", "unknown")) 23 | subject = header_dict.get("Subject", "no subject") 24 | combined = f"{sender}:::{subject}" 25 | return EmailID(combined) 26 | 27 | 28 | class SMTPIMAPMailboxManager(MailboxManager): 29 | """ 30 | A class representing a mailbox manager for SMTP and IMAP protocols. 31 | 32 | Args: 33 | host (str): The hostname of the mail server. 34 | port (int): The port number of the mail server. 35 | username (str): The username for authentication. 36 | password (str): The password for authentication. 37 | """ 38 | 39 | def __init__( 40 | self, 41 | imap_host: str, 42 | imap_port: int, 43 | username: str, 44 | password: str, 45 | smtp_host: str, 46 | smtp_port: int = 465, 47 | smtp_username: str | None = None, 48 | smtp_password: str | None = None, 49 | ): 50 | """Create a new SMTPIMAPMailboxManager object. 51 | 52 | Arguments: 53 | host (str): The hostname of the mail server. 54 | port (int): The port number of the mail server. 55 | username (str): The username for authentication. 56 | password (str): The password for authentication. 57 | 58 | """ 59 | if smtp_username is None: 60 | smtp_username = username 61 | if smtp_password is None: 62 | smtp_password = password 63 | self._box = EmailBox(imap_host, imap_port, username, password) 64 | self._sender = EmailSender(smtp_host, smtp_port, smtp_username, smtp_password) 65 | 66 | @classmethod 67 | def from_file( 68 | cls, path: str | pathlib.Path = "~/.config/epistolary.json" 69 | ) -> "SMTPIMAPMailboxManager": 70 | """Create a new SMTPIMAPMailboxManager from a file. 71 | 72 | Arguments: 73 | path (str | pathlib.Path, optional): The path to the file. 74 | Defaults to "~/.config/epistolary.json". 75 | 76 | Returns: 77 | SMTPIMAPMailboxManager: The mailbox manager. 78 | 79 | """ 80 | config = EpistolaryConfig.from_file(path) 81 | return cls( 82 | imap_host=config.imap_host, 83 | imap_port=config.imap_port, 84 | username=config.email, 85 | password=config.password, 86 | smtp_host=config.smtp_host, 87 | smtp_port=config.smtp_port, 88 | smtp_username=config.smtp_username, 89 | smtp_password=config.smtp_password, 90 | ) 91 | 92 | def get_emails( 93 | self, folder: str | None = None, limit: int | None = None 94 | ) -> dict[EmailID, EmailMessage]: 95 | """ 96 | Get the emails from the specified folder. 97 | 98 | Arguments: 99 | folder (str | None, optional): The folder name. Defaults to "INBOX". 100 | limit (int | None, optional): The maximum number of emails to return. 101 | Defaults to None, which returns all emails. 102 | 103 | Returns: 104 | dict[EmailID, EmailMessage]: A dictionary containing the emails, 105 | where keys are the email IDs and the values are the messages. 106 | """ 107 | if folder is None: 108 | folder = "INBOX" 109 | messages = self._box[folder].search(unseen=True) 110 | result = {} 111 | for message in messages: 112 | result[_get_mail_filename(message.headers)] = message 113 | if limit is not None and len(result) >= limit: 114 | break 115 | return result 116 | 117 | def get_email(self, email_id: EmailID) -> EmailMessage: 118 | """ 119 | Get the email with the specified ID. 120 | 121 | Arguments: 122 | email_id (EmailID): The email ID. 123 | 124 | Returns: 125 | EmailMessage: The email message. 126 | """ 127 | # TODO: Sad puppy. This means we're fetching ALL emails from the server 128 | # every time we want to get a single email. 129 | return self.get_emails()[email_id] 130 | 131 | def get_email_address_and_subject( 132 | self, uid: EmailID, append_re: bool = True 133 | ) -> tuple[str, str]: 134 | """ 135 | Get the email address and subject of the email with the specified ID. 136 | 137 | Arguments: 138 | uid (EmailID): The email ID. 139 | 140 | Returns: 141 | tuple[str, str]: The email address and subject of the email. 142 | """ 143 | try: 144 | email = self.get_email(uid) 145 | return email.from_, ("Re: " + email.subject) if append_re else email.subject 146 | except KeyError: 147 | # Luckily the email ID is literally the address and subject through 148 | # our sanitization function. 149 | dest, subject = uid.split(":::") 150 | return desanitize_fname(dest).strip(), subject.strip() 151 | 152 | def get_email_subject_and_text(self, uid: EmailID) -> tuple[str, str]: 153 | """ 154 | Get the subject and text of the email with the specified ID. 155 | 156 | Arguments: 157 | uid (EmailID): The email ID. 158 | 159 | Returns: 160 | tuple[str, str]: The subject and text of the email. 161 | """ 162 | email = self.get_email(uid) 163 | return email.subject, email.html_body 164 | 165 | def send_message( 166 | self, to: str, subject: str, body: str, in_reply_to: EmailID | None = None 167 | ) -> bool: 168 | """Send a message.""" 169 | # print(f"Sending email to {to} with subject {subject} and body:\n{body}") 170 | # conf = input("Is this correct? (y/n) ") 171 | # if conf.lower() != "y": 172 | # return False 173 | self._sender.send( 174 | subject=subject, 175 | receivers=[to], 176 | text=body, 177 | html=body, 178 | headers={"In-Reply-To": in_reply_to} if in_reply_to is not None else None, 179 | ) 180 | return True 181 | -------------------------------------------------------------------------------- /epistolary/orchestrator.py: -------------------------------------------------------------------------------- 1 | from epistolary.document_manager import DocumentManager 2 | from epistolary.mailbox_manager import MailboxManager 3 | from epistolary.text_extractor import TextExtractor 4 | 5 | import fitz 6 | import io 7 | from fitz import Document 8 | from epistolary.types import DocumentID, EmailID 9 | import base64 10 | 11 | 12 | class EpistolaryOrchestrator: 13 | """A class that orchestrates the Epistolary system.""" 14 | 15 | def __init__( 16 | self, 17 | mailbox_manager: MailboxManager, 18 | document_manager: DocumentManager, 19 | text_extractor: TextExtractor, 20 | debug: bool = False, 21 | ): 22 | """Initialize the orchestrator. 23 | 24 | Arguments: 25 | mailbox_manager: The mailbox manager to use. 26 | document_manager: The document manager to use. 27 | debug: Whether to print debug messages. 28 | 29 | """ 30 | self.mailbox_manager = mailbox_manager 31 | self.document_manager = document_manager 32 | self.text_extractor = text_extractor 33 | self._debug = debug 34 | 35 | def refresh_document_mailbox(self): 36 | """Refresh the document mailbox.""" 37 | new_emails = self.mailbox_manager.get_emails(limit=10) 38 | if self._debug: 39 | print(f"Found {len(new_emails)} new emails.") 40 | 41 | # Delete all old documents: 42 | for document_id in self.document_manager.list_documents(): 43 | # If not in the new emails, delete the document: 44 | if document_id not in new_emails: 45 | if self._debug: 46 | print(f"Deleting document {document_id}: No longer in mailbox.") 47 | self.document_manager.delete_document(document_id) 48 | 49 | # Upload all current emails: 50 | for eid, msg in new_emails.items(): 51 | # Check if the email has already been added to the document mailbox 52 | # (the document ID should be the same as the email ID) 53 | if self.document_manager.has_document(DocumentID(eid)): 54 | if self._debug: 55 | print(f"Skipping email {eid}: Already in mailbox.") 56 | continue 57 | 58 | if ( 59 | "unsubscribe" in (msg.text_body or "").lower() 60 | or "unsubscribe" in (msg.html_body or "").lower() 61 | ): 62 | if self._debug: 63 | print(f"Skipping email {eid}: Unsubscribe link.") 64 | continue 65 | # If the email has not been added, add it: 66 | if self._debug: 67 | print(f"Uploading email {eid} to document mailbox.") 68 | self.upload_email_by_id(eid) 69 | 70 | def _email_to_document(self, email_id: EmailID) -> io.BytesIO: 71 | """Create a document by reflowing the text of an email. 72 | 73 | Arguments: 74 | email_id: The ID of the email to render to PDF. 75 | 76 | """ 77 | email = self.mailbox_manager.get_email(email_id) 78 | sender = email.from_ 79 | subject = email.subject 80 | html_body = email.html_body 81 | text_body = email.text_body 82 | 83 | # text_body is b64 encoded, so we need to decode it: 84 | 85 | try: 86 | text_body = base64.b64decode(text_body).decode("utf-8") 87 | except Exception as _e: 88 | text_body = text_body or html_body 89 | text_body_as_html = text_body.strip().replace("\n", "
") 90 | 91 | # html_body is the HTML content of the email, but it may also be in 92 | # the 93 | # date = email.date 94 | # Render the HTML email to a PDF using the library "fitz": 95 | pagebox = fitz.paper_rect("letter") 96 | story = fitz.Story( 97 | f""" 98 |

{sender}

99 |

{subject}

100 |
101 | {text_body_as_html if text_body_as_html else html_body} 102 | """ 103 | ) 104 | page_with_margins = pagebox + (36, 36, -36, -36) # 0.5in margins 105 | 106 | # Create in-memory PDF: 107 | pdfbytes = io.BytesIO() 108 | writer = fitz.DocumentWriter(pdfbytes) 109 | more = True 110 | while more: 111 | device = writer.begin_page(pagebox) 112 | more, _ = story.place(page_with_margins) 113 | story.draw(device) 114 | writer.end_page() 115 | writer.close() 116 | 117 | pdfbytes.seek(0) 118 | return pdfbytes 119 | 120 | def upload_email_by_id(self, email_id: EmailID) -> DocumentID: 121 | """Upload an email to the document manager. 122 | 123 | Arguments: 124 | email_id: The ID of the email to upload. 125 | 126 | Returns: 127 | The ID of the document. 128 | 129 | """ 130 | # Create a document from the subject and text and then append a page 131 | # for the user to write on 132 | document_bytes = self._email_to_document(email_id) 133 | document = fitz.Document(stream=document_bytes.read(), filetype="pdf") 134 | 135 | document = self.document_manager.append_ruled_page_to_document(document) 136 | # Put the document into the document manager: 137 | document_id = self.document_manager.put_document(document, email_id) 138 | return document_id 139 | 140 | def get_edited_documents(self) -> dict[DocumentID, Document]: 141 | """Get all documents that have been edited.""" 142 | docs = self.document_manager.get_edited_documents() 143 | return docs 144 | 145 | def get_last_page_ocr_text_for_document(self, doc: DocumentID | Document) -> str: 146 | """Get the OCR text for the last page of a document. 147 | 148 | Arguments: 149 | doc: The document to get the OCR text for. This can be either a 150 | document ID or a document object. If it is a document ID, the 151 | document will be retrieved from the document manager. 152 | 153 | Returns: 154 | The OCR text for the last page of the document. 155 | 156 | """ 157 | if isinstance(doc, DocumentID): 158 | doc = self.document_manager.get_document(doc) 159 | last_page = doc[-1] 160 | return self.text_extractor.extract_text_from_page(last_page) 161 | 162 | def send_outbox(self) -> list[EmailID]: 163 | """Send all documents in the outbox.""" 164 | outbox = self.document_manager.get_edited_documents() 165 | sent_emails = [] 166 | for did, doc in outbox.items(): 167 | outgoing_text = self.get_last_page_ocr_text_for_document(doc) 168 | addr, subj = self.mailbox_manager.get_email_address_and_subject(did) 169 | result = self.mailbox_manager.send_message( 170 | to=addr, 171 | subject=subj, 172 | body=outgoing_text, 173 | in_reply_to=did, 174 | ) 175 | sent_emails.append(result) 176 | if result: 177 | self.document_manager.delete_document(did) 178 | return sent_emails 179 | -------------------------------------------------------------------------------- /epistolary/remarkable/__init__.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import pathlib 3 | import tempfile 4 | import subprocess 5 | 6 | 7 | class ReMarkablePathType(enum.Enum): 8 | """Enum for the type of path on the reMarkable.""" 9 | 10 | FILE = "[f]" 11 | DIRECTORY = "[d]" 12 | 13 | def __str__(self): 14 | return self.value 15 | 16 | 17 | class RMAPIWrapper: 18 | """A wrapper around the rmapi tool.""" 19 | 20 | def __init__( 21 | self, 22 | confirm_rmapi: bool = True, 23 | rmapi_path: str = "rmapi", 24 | cache_dir: str | pathlib.Path | None = None, 25 | ): 26 | self._rmapi_path = rmapi_path 27 | self._cache_dir = ( 28 | pathlib.Path(cache_dir) 29 | if cache_dir 30 | else pathlib.Path(tempfile.gettempdir()) 31 | ) 32 | 33 | if confirm_rmapi: 34 | self._confirm_rmapi() 35 | 36 | def _run_rmapi(self, *args): 37 | """ 38 | Run the rmapi tool with the given arguments. 39 | """ 40 | try: 41 | return subprocess.run( 42 | [self._rmapi_path, *args], 43 | stdout=subprocess.PIPE, 44 | stderr=subprocess.PIPE, 45 | check=True, 46 | cwd=self._cache_dir, 47 | ) 48 | except subprocess.CalledProcessError as e: 49 | raise Exception(e.stderr.decode("utf-8")) from e 50 | 51 | def _confirm_rmapi(self): 52 | """ 53 | Confirm that the rmapi tool is installed and running. 54 | """ 55 | try: 56 | self._run_rmapi("version") 57 | except FileNotFoundError: 58 | raise FileNotFoundError(f"rmapi tool not found at {self._rmapi_path}") 59 | 60 | def ls(self, remote_path: str = "") -> list[tuple[ReMarkablePathType, str]]: 61 | """ 62 | List files in the remote path. 63 | """ 64 | result = self._run_rmapi("ls", remote_path) 65 | printout = result.stdout.decode("utf-8").split("\n") 66 | files = [] 67 | for line in printout: 68 | if line: 69 | path_type, path = line.split("\t") 70 | files.append((ReMarkablePathType(path_type), path)) 71 | return files 72 | 73 | def download(self, remote_path: str, local_path: pathlib.Path): 74 | """ 75 | Download a file from the reMarkable. 76 | """ 77 | # remove extension from local path 78 | local_path = pathlib.Path( 79 | str(local_path).replace(".pdf", "").replace(".zip", "") 80 | ) 81 | # Create the local path if it doesn't exist 82 | local_path.parent.mkdir(parents=True, exist_ok=True) 83 | self._run_rmapi("get", remote_path) 84 | # print("DEBUG: Listing files in cache directory:") 85 | # for f in self._cache_dir.iterdir(): 86 | # print("DEBUG: file in cache:", f) 87 | base_fname = pathlib.Path(remote_path).name 88 | # print(f"DEBUG: Cache directory: {self._cache_dir}") 89 | # print(f"DEBUG: Base file name: {base_fname}") 90 | matching_zips = list(self._cache_dir.rglob(f"*{base_fname}*.zip")) 91 | if not matching_zips: 92 | matching_rmdocs = list(self._cache_dir.rglob(f"*{base_fname}*.rmdoc")) 93 | if matching_rmdocs: 94 | print(f"DEBUG: Found rmdoc file: {matching_rmdocs[0]}, renaming to zip") 95 | matching_rmdocs[0].rename(pathlib.Path(str(local_path) + ".zip")) 96 | else: 97 | return 98 | # all_zips = list(self._cache_dir.rglob("*.zip")) 99 | # if len(all_zips) == 1: 100 | # print( 101 | # f"DEBUG: Fallback: using the only zip file found: {all_zips[0]}" 102 | # ) 103 | # all_zips[0].rename(pathlib.Path(str(local_path) + ".zip")) 104 | # else: 105 | # print( 106 | # f"ERROR: No matching zip or rmdoc file found in {self._cache_dir} for base name {base_fname}" 107 | # ) 108 | # return 109 | else: 110 | zip_file = matching_zips[0] 111 | print(f"DEBUG: Found zip file: {zip_file}") 112 | zip_file.rename(pathlib.Path(str(local_path) + ".zip")) 113 | 114 | def upload(self, local_path: pathlib.Path, remote: str): 115 | """ 116 | Upload a file to the reMarkable. 117 | """ 118 | self._run_rmapi("put", str(local_path), remote) 119 | 120 | def delete(self, remote_path: str): 121 | """ 122 | Delete a file from the reMarkable. 123 | """ 124 | self._run_rmapi("rm", remote_path) 125 | 126 | def mkdir(self, remote_path: str): 127 | """ 128 | Create a directory on the reMarkable. 129 | """ 130 | self._run_rmapi("mkdir", remote_path) 131 | 132 | def move(self, input_remote_path: str, output_remote_path: str): 133 | """ 134 | Rename a reMarkable file. 135 | """ 136 | self._run_rmapi("mv", input_remote_path, output_remote_path) 137 | 138 | 139 | class RemarksWrapper: 140 | """ 141 | The remarks tool is a command line tool that can be used to interpret the 142 | reMarkable rmv6 files and convert them to, say, PDF. 143 | """ 144 | 145 | def __init__( 146 | self, 147 | confirm_remarks: bool = True, 148 | remarks_path: str = "python3 -m remarks", 149 | debug: bool = False, 150 | ): 151 | self._remarks_path = remarks_path 152 | self._debug = debug 153 | 154 | if confirm_remarks: 155 | self._confirm_remarks() 156 | 157 | def _run_remarks(self, *args): 158 | """ 159 | Run the remarks tool with the given arguments. 160 | """ 161 | if self._debug: 162 | print(f"[REMARKS] {self._remarks_path} {' '.join(args)}") 163 | try: 164 | return subprocess.run( 165 | [*self._remarks_path.split(" "), *args], 166 | stdout=subprocess.PIPE, 167 | stderr=subprocess.PIPE, 168 | check=True, 169 | ) 170 | except subprocess.CalledProcessError as e: 171 | raise Exception(e.stderr.decode("utf-8")) from e 172 | 173 | def _confirm_remarks(self): 174 | """ 175 | Confirm that the remarks tool is installed and running. 176 | """ 177 | try: 178 | self._run_remarks("--version") 179 | except FileNotFoundError: 180 | raise FileNotFoundError(f"remarks tool not found at {self._remarks_path}") 181 | 182 | def rm_to_pdf( 183 | self, input_path: str | pathlib.Path, output_path: str | pathlib.Path 184 | ): 185 | """ 186 | Convert a reMarkable file to PDF. 187 | """ 188 | input_path = pathlib.Path(input_path) 189 | output_path = pathlib.Path(output_path) 190 | # Create output path parent if it doesn't exist 191 | output_path.parent.mkdir(parents=True, exist_ok=True) 192 | output_path_without_extension = output_path.with_suffix("") 193 | # If input path is a .zip, extract it 194 | if not input_path.suffix == ".zip": 195 | raise NotImplementedError("Input path must be a .zip file.") 196 | temp_dir = tempfile.mkdtemp() 197 | subprocess.run(["unzip", str(input_path), "-d", temp_dir], check=True) 198 | self._run_remarks(str(temp_dir), str(output_path_without_extension)) 199 | 200 | # The output path (without ext) is a dir. There is a pdf inside of 201 | # it that we need to move to the correct location: 202 | print(list(output_path_without_extension.glob("*.pdf"))) 203 | list(output_path_without_extension.glob("*.pdf"))[0].rename( 204 | str(output_path.parent / output_path.name) 205 | ) 206 | -------------------------------------------------------------------------------- /epistolary/text_extractor/__init__.py: -------------------------------------------------------------------------------- 1 | from .text_extractor import TextExtractor 2 | from .tesseract_text_extractor import TesseractTextExtractor 3 | 4 | __all__ = ["TextExtractor", "TesseractTextExtractor"] 5 | -------------------------------------------------------------------------------- /epistolary/text_extractor/openai_text_extractor.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from fitz import Page 3 | from openai import OpenAI 4 | 5 | from .text_extractor import TextExtractor 6 | 7 | 8 | class OpenAITextExtractor(TextExtractor): 9 | def __init__( 10 | self, 11 | client: OpenAI | None = None, 12 | api_key: str | None = None, 13 | page_shrink_factor: int = 2, 14 | ): 15 | self._client = client or (OpenAI(api_key=api_key) if api_key else OpenAI()) 16 | self._shrink_factor = page_shrink_factor 17 | 18 | def extract_text_from_page(self, page: Page) -> str: 19 | png = page.get_pixmap() 20 | png.shrink(self._shrink_factor) 21 | png_bytes = png.tobytes("png") 22 | png_b64 = base64.b64encode(png_bytes).decode("utf-8") 23 | 24 | res = self._client.chat.completions.create( 25 | model="gpt-4o-mini", 26 | messages=[ 27 | { 28 | "role": "system", 29 | "content": "You are a powerful handwriting parser robot. You respond ONLY with the transcribed text, NEVER with any other information, questions, or discussion.", 30 | }, 31 | { 32 | "role": "user", 33 | "content": [ 34 | { 35 | "type": "text", 36 | "text": "What is the (formatted) text content of this hand-written message?", 37 | }, 38 | { 39 | "type": "image_url", 40 | "image_url": { 41 | "url": f"data:image/jpeg;base64,{png_b64}", 42 | }, 43 | }, 44 | ], 45 | }, 46 | ], 47 | ) 48 | if len(res.choices) and res.choices[0].message.role == "assistant": 49 | return ( 50 | res.choices[0].message.content if res.choices[0].message.content else "" 51 | ) 52 | raise ValueError("No response from OpenAI") 53 | -------------------------------------------------------------------------------- /epistolary/text_extractor/tesseract_text_extractor.py: -------------------------------------------------------------------------------- 1 | import pytesseract 2 | import fitz 3 | from PIL import Image 4 | 5 | from .text_extractor import TextExtractor 6 | 7 | 8 | class TesseractTextExtractor(TextExtractor): 9 | """A class that extracts text from documents using Tesseract.""" 10 | 11 | def extract_text_from_page(self, page: fitz.Page) -> str: 12 | """Extract text from a page. 13 | 14 | Arguments: 15 | page: The page to extract text from. 16 | 17 | Returns: 18 | The extracted text. 19 | 20 | """ 21 | # Convert the PDF page to be readable as an image, using the 22 | # fitz library. 23 | pix = page.get_pixmap() # type: ignore 24 | img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples) 25 | return pytesseract.image_to_string(img) 26 | -------------------------------------------------------------------------------- /epistolary/text_extractor/text_extractor.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | from fitz import Page 4 | 5 | 6 | class TextExtractor(Protocol): 7 | """A protocol for classes that extract text from documents.""" 8 | 9 | def extract_text_from_page(self, page: Page) -> str: 10 | """Extract text from a page. 11 | 12 | Arguments: 13 | page: The page to extract text from. 14 | 15 | Returns: 16 | The extracted text. 17 | 18 | """ 19 | ... 20 | -------------------------------------------------------------------------------- /epistolary/types.py: -------------------------------------------------------------------------------- 1 | EmailID = str 2 | DocumentID = str 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "epistolary" 3 | version = "0.2.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "redbox>=0.2.1", 9 | "redmail>=0.6.0", 10 | "pytesseract>=0.3.13", 11 | "pillow>=10.4.0", 12 | "remarks", 13 | "boto3>=1.35.8", 14 | "openai>=1.43.0", 15 | "click>=8.1.7", 16 | ] 17 | 18 | [build-system] 19 | requires = ["hatchling"] 20 | build-backend = "hatchling.build" 21 | 22 | [tool.uv] 23 | dev-dependencies = [ 24 | "ipykernel>=6.29.5", 25 | "ruff>=0.6.3", 26 | ] 27 | 28 | [tool.uv.sources] 29 | remarks = { git = "https://github.com/azeirah/remarks", branch = "v6_with_rmscene" } 30 | 31 | [project.scripts] 32 | epistolary = "epistolary.cli:cli" -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.11" 3 | resolution-markers = [ 4 | "python_full_version < '3.13'", 5 | "python_full_version >= '3.13'", 6 | ] 7 | 8 | [[package]] 9 | name = "annotated-types" 10 | version = "0.7.0" 11 | source = { registry = "https://pypi.org/simple" } 12 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 13 | wheels = [ 14 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 15 | ] 16 | 17 | [[package]] 18 | name = "anyio" 19 | version = "4.4.0" 20 | source = { registry = "https://pypi.org/simple" } 21 | dependencies = [ 22 | { name = "idna" }, 23 | { name = "sniffio" }, 24 | ] 25 | sdist = { url = "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", size = 163930 } 26 | wheels = [ 27 | { url = "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", size = 86780 }, 28 | ] 29 | 30 | [[package]] 31 | name = "appnope" 32 | version = "0.1.4" 33 | source = { registry = "https://pypi.org/simple" } 34 | sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } 35 | wheels = [ 36 | { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, 37 | ] 38 | 39 | [[package]] 40 | name = "asttokens" 41 | version = "2.4.1" 42 | source = { registry = "https://pypi.org/simple" } 43 | dependencies = [ 44 | { name = "six" }, 45 | ] 46 | sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284 } 47 | wheels = [ 48 | { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, 49 | ] 50 | 51 | [[package]] 52 | name = "boto3" 53 | version = "1.35.8" 54 | source = { registry = "https://pypi.org/simple" } 55 | dependencies = [ 56 | { name = "botocore" }, 57 | { name = "jmespath" }, 58 | { name = "s3transfer" }, 59 | ] 60 | sdist = { url = "https://files.pythonhosted.org/packages/91/64/39a9c9c490ad4201a88a2f3339286eedb9200cf4868b3917c002aef02233/boto3-1.35.8.tar.gz", hash = "sha256:b9587131372a808bf6f99c5ed8b11be55cd113261cc3b437a917b4acc6c30bfe", size = 108616 } 61 | wheels = [ 62 | { url = "https://files.pythonhosted.org/packages/d9/cd/db2b57967b189a5fa355887581457df008003671c41725d744ca8b8b507e/boto3-1.35.8-py3-none-any.whl", hash = "sha256:06eac4757de2a9c6020381205cb902f05964caad80b56e58c8931284a133b4cb", size = 139144 }, 63 | ] 64 | 65 | [[package]] 66 | name = "botocore" 67 | version = "1.35.8" 68 | source = { registry = "https://pypi.org/simple" } 69 | dependencies = [ 70 | { name = "jmespath" }, 71 | { name = "python-dateutil" }, 72 | { name = "urllib3" }, 73 | ] 74 | sdist = { url = "https://files.pythonhosted.org/packages/00/cd/7d9250eb9734ce3c220178a63ad87ae4dd8019c1c48d48a7f014b84b9ead/botocore-1.35.8.tar.gz", hash = "sha256:4b820cf680ab5d778bd2fe4feeef1ff8a2b96d5c535d4638ab30f703ade282f8", size = 12700788 } 75 | wheels = [ 76 | { url = "https://files.pythonhosted.org/packages/6d/e4/62d781901fa1543f3274b259b15887e957927f3422e379f04244fae1accf/botocore-1.35.8-py3-none-any.whl", hash = "sha256:adf389eb8fd87775f193300e3431d1353f925807ad3a39958172cb644f0d60a1", size = 12491569 }, 77 | ] 78 | 79 | [[package]] 80 | name = "certifi" 81 | version = "2024.7.4" 82 | source = { registry = "https://pypi.org/simple" } 83 | sdist = { url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", size = 164065 } 84 | wheels = [ 85 | { url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", size = 162960 }, 86 | ] 87 | 88 | [[package]] 89 | name = "cffi" 90 | version = "1.17.0" 91 | source = { registry = "https://pypi.org/simple" } 92 | dependencies = [ 93 | { name = "pycparser" }, 94 | ] 95 | sdist = { url = "https://files.pythonhosted.org/packages/1e/bf/82c351342972702867359cfeba5693927efe0a8dd568165490144f554b18/cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", size = 516073 } 96 | wheels = [ 97 | { url = "https://files.pythonhosted.org/packages/53/cc/9298fb6235522e00e47d78d6aa7f395332ef4e5f6fe124f9a03aa60600f7/cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720", size = 181912 }, 98 | { url = "https://files.pythonhosted.org/packages/e7/79/dc5334fbe60635d0846c56597a8d2af078a543ff22bc48d36551a0de62c2/cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9", size = 178297 }, 99 | { url = "https://files.pythonhosted.org/packages/39/d7/ef1b6b16b51ccbabaced90ff0d821c6c23567fc4b2e4a445aea25d3ceb92/cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb", size = 444909 }, 100 | { url = "https://files.pythonhosted.org/packages/29/b8/6e3c61885537d985c78ef7dd779b68109ba256263d74a2f615c40f44548d/cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424", size = 468854 }, 101 | { url = "https://files.pythonhosted.org/packages/0b/49/adad1228e19b931e523c2731e6984717d5f9e33a2f9971794ab42815b29b/cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d", size = 476890 }, 102 | { url = "https://files.pythonhosted.org/packages/76/54/c00f075c3e7fd14d9011713bcdb5b4f105ad044c5ad948db7b1a0a7e4e78/cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8", size = 459374 }, 103 | { url = "https://files.pythonhosted.org/packages/f3/b9/f163bb3fa4fbc636ee1f2a6a4598c096cdef279823ddfaa5734e556dd206/cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6", size = 466891 }, 104 | { url = "https://files.pythonhosted.org/packages/31/52/72bbc95f6d06ff2e88a6fa13786be4043e542cb24748e1351aba864cb0a7/cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91", size = 477658 }, 105 | { url = "https://files.pythonhosted.org/packages/67/20/d694811457eeae0c7663fa1a7ca201ce495533b646c1180d4ac25684c69c/cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8", size = 453890 }, 106 | { url = "https://files.pythonhosted.org/packages/dc/79/40cbf5739eb4f694833db5a27ce7f63e30a9b25b4a836c4f25fb7272aacc/cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb", size = 478254 }, 107 | { url = "https://files.pythonhosted.org/packages/e9/eb/2c384c385cca5cae67ca10ac4ef685277680b8c552b99aedecf4ea23ff7e/cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9", size = 171285 }, 108 | { url = "https://files.pythonhosted.org/packages/ca/42/74cb1e0f1b79cb64672f3cb46245b506239c1297a20c0d9c3aeb3929cb0c/cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0", size = 180842 }, 109 | { url = "https://files.pythonhosted.org/packages/1a/1f/7862231350cc959a3138889d2c8d33da7042b22e923457dfd4cd487d772a/cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", size = 182826 }, 110 | { url = "https://files.pythonhosted.org/packages/8b/8c/26119bf8b79e05a1c39812064e1ee7981e1f8a5372205ba5698ea4dd958d/cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", size = 178494 }, 111 | { url = "https://files.pythonhosted.org/packages/61/94/4882c47d3ad396d91f0eda6ef16d45be3d752a332663b7361933039ed66a/cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", size = 454459 }, 112 | { url = "https://files.pythonhosted.org/packages/0f/7c/a6beb119ad515058c5ee1829742d96b25b2b9204ff920746f6e13bf574eb/cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", size = 478502 }, 113 | { url = "https://files.pythonhosted.org/packages/61/8a/2575cd01a90e1eca96a30aec4b1ac101a6fae06c49d490ac2704fa9bc8ba/cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", size = 485381 }, 114 | { url = "https://files.pythonhosted.org/packages/cd/66/85899f5a9f152db49646e0c77427173e1b77a1046de0191ab3b0b9a5e6e3/cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", size = 470907 }, 115 | { url = "https://files.pythonhosted.org/packages/00/13/150924609bf377140abe6e934ce0a57f3fc48f1fd956ec1f578ce97a4624/cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", size = 479074 }, 116 | { url = "https://files.pythonhosted.org/packages/17/fd/7d73d7110155c036303b0a6462c56250e9bc2f4119d7591d27417329b4d1/cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", size = 484225 }, 117 | { url = "https://files.pythonhosted.org/packages/fc/83/8353e5c9b01bb46332dac3dfb18e6c597a04ceb085c19c814c2f78a8c0d0/cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", size = 488388 }, 118 | { url = "https://files.pythonhosted.org/packages/73/0c/f9d5ca9a095b1fc88ef77d1f8b85d11151c374144e4606da33874e17b65b/cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492", size = 172096 }, 119 | { url = "https://files.pythonhosted.org/packages/72/21/8c5d285fe20a6e31d29325f1287bb0e55f7d93630a5a44cafdafb5922495/cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2", size = 181478 }, 120 | { url = "https://files.pythonhosted.org/packages/17/8f/581f2f3c3464d5f7cf87c2f7a5ba9acc6976253e02d73804240964243ec2/cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118", size = 182638 }, 121 | { url = "https://files.pythonhosted.org/packages/8d/1c/c9afa66684b7039f48018eb11b229b659dfb32b7a16b88251bac106dd1ff/cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7", size = 178453 }, 122 | { url = "https://files.pythonhosted.org/packages/cc/b6/1a134d479d3a5a1ff2fabbee551d1d3f1dd70f453e081b5f70d604aae4c0/cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377", size = 454441 }, 123 | { url = "https://files.pythonhosted.org/packages/b1/b4/e1569475d63aad8042b0935dbf62ae2a54d1e9142424e2b0e924d2d4a529/cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb", size = 478543 }, 124 | { url = "https://files.pythonhosted.org/packages/d2/40/a9ad03fbd64309dec5bb70bc803a9a6772602de0ee164d7b9a6ca5a89249/cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555", size = 485463 }, 125 | { url = "https://files.pythonhosted.org/packages/a6/1a/f10be60e006dd9242a24bcc2b1cd55c34c578380100f742d8c610f7a5d26/cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204", size = 470854 }, 126 | { url = "https://files.pythonhosted.org/packages/cc/b3/c035ed21aa3d39432bd749fe331ee90e4bc83ea2dbed1f71c4bc26c41084/cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f", size = 479096 }, 127 | { url = "https://files.pythonhosted.org/packages/00/cb/6f7edde01131de9382c89430b8e253b8c8754d66b63a62059663ceafeab2/cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0", size = 484013 }, 128 | { url = "https://files.pythonhosted.org/packages/b9/83/8e4e8c211ea940210d293e951bf06b1bfb90f2eeee590e9778e99b4a8676/cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4", size = 488119 }, 129 | { url = "https://files.pythonhosted.org/packages/5e/52/3f7cfbc4f444cb4f73ff17b28690d12436dde665f67d68f1e1687908ab6c/cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a", size = 172122 }, 130 | { url = "https://files.pythonhosted.org/packages/94/19/cf5baa07ee0f0e55eab7382459fbddaba0fdb0ba45973dd92556ae0d02db/cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7", size = 181504 }, 131 | ] 132 | 133 | [[package]] 134 | name = "click" 135 | version = "8.1.7" 136 | source = { registry = "https://pypi.org/simple" } 137 | dependencies = [ 138 | { name = "colorama", marker = "platform_system == 'Windows'" }, 139 | ] 140 | sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } 141 | wheels = [ 142 | { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, 143 | ] 144 | 145 | [[package]] 146 | name = "colorama" 147 | version = "0.4.6" 148 | source = { registry = "https://pypi.org/simple" } 149 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 150 | wheels = [ 151 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 152 | ] 153 | 154 | [[package]] 155 | name = "comm" 156 | version = "0.2.2" 157 | source = { registry = "https://pypi.org/simple" } 158 | dependencies = [ 159 | { name = "traitlets" }, 160 | ] 161 | sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210 } 162 | wheels = [ 163 | { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, 164 | ] 165 | 166 | [[package]] 167 | name = "debugpy" 168 | version = "1.8.5" 169 | source = { registry = "https://pypi.org/simple" } 170 | sdist = { url = "https://files.pythonhosted.org/packages/ea/f9/61c325a10ded8dc3ddc3e7cd2ed58c0b15b2ef4bf8b4bf2930ee98ed59ee/debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0", size = 4612118 } 171 | wheels = [ 172 | { url = "https://files.pythonhosted.org/packages/ad/72/fd138a10dda16775607316d60dd440fcd23e7560e9276da53c597b5917e9/debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a", size = 1786504 }, 173 | { url = "https://files.pythonhosted.org/packages/e2/0e/d0e6af2d7bbf5ace847e4d3bd41f8f9d4a0764fcd8058f07a1c51618cbf2/debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b", size = 2642077 }, 174 | { url = "https://files.pythonhosted.org/packages/f6/55/2a1dc192894ba9b368cdcce15315761a00f2d4cd7de4402179648840e480/debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408", size = 4702081 }, 175 | { url = "https://files.pythonhosted.org/packages/7f/7f/942b23d64f4896e9f8776cf306dfd00feadc950a38d56398610a079b28b1/debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3", size = 4715571 }, 176 | { url = "https://files.pythonhosted.org/packages/9a/82/7d9e1f75fb23c876ab379008c7cf484a1cfa5ed47ccaac8ba37c75e6814e/debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156", size = 1436398 }, 177 | { url = "https://files.pythonhosted.org/packages/fd/b6/ee71d5e73712daf8307a9e85f5e39301abc8b66d13acd04dfff1702e672e/debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb", size = 1437465 }, 178 | { url = "https://files.pythonhosted.org/packages/6c/d8/8e32bf1f2e0142f7e8a2c354338b493e87f2c44e77e233b3a140fb5efa03/debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7", size = 4581313 }, 179 | { url = "https://files.pythonhosted.org/packages/f7/be/2fbaffecb063de228b2b3b6a1750b0b745e5dc645eddd52be8b329933c0b/debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c", size = 4581209 }, 180 | { url = "https://files.pythonhosted.org/packages/02/49/b595c34d7bc690e8d225a6641618a5c111c7e13db5d9e2b756c15ce8f8c6/debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44", size = 4824118 }, 181 | ] 182 | 183 | [[package]] 184 | name = "decorator" 185 | version = "5.1.1" 186 | source = { registry = "https://pypi.org/simple" } 187 | sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } 188 | wheels = [ 189 | { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, 190 | ] 191 | 192 | [[package]] 193 | name = "distro" 194 | version = "1.9.0" 195 | source = { registry = "https://pypi.org/simple" } 196 | sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } 197 | wheels = [ 198 | { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, 199 | ] 200 | 201 | [[package]] 202 | name = "epistolary" 203 | version = "0.1.0" 204 | source = { editable = "." } 205 | dependencies = [ 206 | { name = "boto3" }, 207 | { name = "click" }, 208 | { name = "openai" }, 209 | { name = "pillow" }, 210 | { name = "pytesseract" }, 211 | { name = "redbox" }, 212 | { name = "redmail" }, 213 | { name = "remarks" }, 214 | ] 215 | 216 | [package.dev-dependencies] 217 | dev = [ 218 | { name = "ipykernel" }, 219 | { name = "ruff" }, 220 | ] 221 | 222 | [package.metadata] 223 | requires-dist = [ 224 | { name = "boto3", specifier = ">=1.35.8" }, 225 | { name = "click", specifier = ">=8.1.7" }, 226 | { name = "openai", specifier = ">=1.43.0" }, 227 | { name = "pillow", specifier = ">=10.4.0" }, 228 | { name = "pytesseract", specifier = ">=0.3.13" }, 229 | { name = "redbox", specifier = ">=0.2.1" }, 230 | { name = "redmail", specifier = ">=0.6.0" }, 231 | { name = "remarks", git = "https://github.com/azeirah/remarks?branch=v6_with_rmscene" }, 232 | ] 233 | 234 | [package.metadata.requires-dev] 235 | dev = [ 236 | { name = "ipykernel", specifier = ">=6.29.5" }, 237 | { name = "ruff", specifier = ">=0.6.3" }, 238 | ] 239 | 240 | [[package]] 241 | name = "executing" 242 | version = "2.0.1" 243 | source = { registry = "https://pypi.org/simple" } 244 | sdist = { url = "https://files.pythonhosted.org/packages/08/41/85d2d28466fca93737592b7f3cc456d1cfd6bcd401beceeba17e8e792b50/executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147", size = 836501 } 245 | wheels = [ 246 | { url = "https://files.pythonhosted.org/packages/80/03/6ea8b1b2a5ab40a7a60dc464d3daa7aa546e0a74d74a9f8ff551ea7905db/executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc", size = 24922 }, 247 | ] 248 | 249 | [[package]] 250 | name = "h11" 251 | version = "0.14.0" 252 | source = { registry = "https://pypi.org/simple" } 253 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 254 | wheels = [ 255 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 256 | ] 257 | 258 | [[package]] 259 | name = "httpcore" 260 | version = "1.0.5" 261 | source = { registry = "https://pypi.org/simple" } 262 | dependencies = [ 263 | { name = "certifi" }, 264 | { name = "h11" }, 265 | ] 266 | sdist = { url = "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", size = 83234 } 267 | wheels = [ 268 | { url = "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", size = 77926 }, 269 | ] 270 | 271 | [[package]] 272 | name = "httpx" 273 | version = "0.27.2" 274 | source = { registry = "https://pypi.org/simple" } 275 | dependencies = [ 276 | { name = "anyio" }, 277 | { name = "certifi" }, 278 | { name = "httpcore" }, 279 | { name = "idna" }, 280 | { name = "sniffio" }, 281 | ] 282 | sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } 283 | wheels = [ 284 | { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, 285 | ] 286 | 287 | [[package]] 288 | name = "idna" 289 | version = "3.8" 290 | source = { registry = "https://pypi.org/simple" } 291 | sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/e349c5e6d4543326c6883ee9491e3921e0d07b55fdf3cce184b40d63e72a/idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603", size = 189467 } 292 | wheels = [ 293 | { url = "https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", size = 66894 }, 294 | ] 295 | 296 | [[package]] 297 | name = "iniconfig" 298 | version = "2.0.0" 299 | source = { registry = "https://pypi.org/simple" } 300 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 301 | wheels = [ 302 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 303 | ] 304 | 305 | [[package]] 306 | name = "ipykernel" 307 | version = "6.29.5" 308 | source = { registry = "https://pypi.org/simple" } 309 | dependencies = [ 310 | { name = "appnope", marker = "platform_system == 'Darwin'" }, 311 | { name = "comm" }, 312 | { name = "debugpy" }, 313 | { name = "ipython" }, 314 | { name = "jupyter-client" }, 315 | { name = "jupyter-core" }, 316 | { name = "matplotlib-inline" }, 317 | { name = "nest-asyncio" }, 318 | { name = "packaging" }, 319 | { name = "psutil" }, 320 | { name = "pyzmq" }, 321 | { name = "tornado" }, 322 | { name = "traitlets" }, 323 | ] 324 | sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367 } 325 | wheels = [ 326 | { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173 }, 327 | ] 328 | 329 | [[package]] 330 | name = "ipython" 331 | version = "8.26.0" 332 | source = { registry = "https://pypi.org/simple" } 333 | dependencies = [ 334 | { name = "colorama", marker = "sys_platform == 'win32'" }, 335 | { name = "decorator" }, 336 | { name = "jedi" }, 337 | { name = "matplotlib-inline" }, 338 | { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, 339 | { name = "prompt-toolkit" }, 340 | { name = "pygments" }, 341 | { name = "stack-data" }, 342 | { name = "traitlets" }, 343 | { name = "typing-extensions", marker = "python_full_version < '3.12'" }, 344 | ] 345 | sdist = { url = "https://files.pythonhosted.org/packages/7e/f4/dc45805e5c3e327a626139c023b296bafa4537e602a61055d377704ca54c/ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c", size = 5493422 } 346 | wheels = [ 347 | { url = "https://files.pythonhosted.org/packages/73/48/4d2818054671bb272d1b12ca65748a4145dc602a463683b5c21b260becee/ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff", size = 817939 }, 348 | ] 349 | 350 | [[package]] 351 | name = "jedi" 352 | version = "0.19.1" 353 | source = { registry = "https://pypi.org/simple" } 354 | dependencies = [ 355 | { name = "parso" }, 356 | ] 357 | sdist = { url = "https://files.pythonhosted.org/packages/d6/99/99b493cec4bf43176b678de30f81ed003fd6a647a301b9c927280c600f0a/jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", size = 1227821 } 358 | wheels = [ 359 | { url = "https://files.pythonhosted.org/packages/20/9f/bc63f0f0737ad7a60800bfd472a4836661adae21f9c2535f3957b1e54ceb/jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0", size = 1569361 }, 360 | ] 361 | 362 | [[package]] 363 | name = "jinja2" 364 | version = "3.1.4" 365 | source = { registry = "https://pypi.org/simple" } 366 | dependencies = [ 367 | { name = "markupsafe" }, 368 | ] 369 | sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } 370 | wheels = [ 371 | { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, 372 | ] 373 | 374 | [[package]] 375 | name = "jiter" 376 | version = "0.5.0" 377 | source = { registry = "https://pypi.org/simple" } 378 | sdist = { url = "https://files.pythonhosted.org/packages/d7/1a/aa64be757afc614484b370a4d9fc1747dc9237b37ce464f7f9d9ca2a3d38/jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a", size = 158300 } 379 | wheels = [ 380 | { url = "https://files.pythonhosted.org/packages/94/5f/3ac960ed598726aae46edea916e6df4df7ff6fe084bc60774b95cf3154e6/jiter-0.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4c8e1ed0ef31ad29cae5ea16b9e41529eb50a7fba70600008e9f8de6376d553", size = 284131 }, 381 | { url = "https://files.pythonhosted.org/packages/03/eb/2308fa5f5c14c97c4c7720fef9465f1fa0771826cddb4eec9866bdd88846/jiter-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6f16e21276074a12d8421692515b3fd6d2ea9c94fd0734c39a12960a20e85f3", size = 299310 }, 382 | { url = "https://files.pythonhosted.org/packages/3c/f6/dba34ca10b44715fa5302b8e8d2113f72eb00a9297ddf3fa0ae4fd22d1d1/jiter-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5280e68e7740c8c128d3ae5ab63335ce6d1fb6603d3b809637b11713487af9e6", size = 332282 }, 383 | { url = "https://files.pythonhosted.org/packages/69/f7/64e0a7439790ec47f7681adb3871c9d9c45fff771102490bbee5e92c00b7/jiter-0.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:583c57fc30cc1fec360e66323aadd7fc3edeec01289bfafc35d3b9dcb29495e4", size = 342370 }, 384 | { url = "https://files.pythonhosted.org/packages/55/31/1efbfff2ae8e4d919144c53db19b828049ad0622a670be3bbea94a86282c/jiter-0.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26351cc14507bdf466b5f99aba3df3143a59da75799bf64a53a3ad3155ecded9", size = 363591 }, 385 | { url = "https://files.pythonhosted.org/packages/30/c3/7ab2ca2276426a7398c6dfb651e38dbc81954c79a3bfbc36c514d8599499/jiter-0.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829df14d656b3fb87e50ae8b48253a8851c707da9f30d45aacab2aa2ba2d614", size = 378551 }, 386 | { url = "https://files.pythonhosted.org/packages/47/e7/5d88031cd743c62199b125181a591b1671df3ff2f6e102df85c58d8f7d31/jiter-0.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42a4bdcf7307b86cb863b2fb9bb55029b422d8f86276a50487982d99eed7c6e", size = 319152 }, 387 | { url = "https://files.pythonhosted.org/packages/4c/2d/09ea58e1adca9f0359f3d41ef44a1a18e59518d7c43a21f4ece9e72e28c0/jiter-0.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04d461ad0aebf696f8da13c99bc1b3e06f66ecf6cfd56254cc402f6385231c06", size = 357377 }, 388 | { url = "https://files.pythonhosted.org/packages/7d/2f/83ff1058cb56fc3ff73e0d3c6440703ddc9cdb7f759b00cfbde8228fc435/jiter-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6375923c5f19888c9226582a124b77b622f8fd0018b843c45eeb19d9701c403", size = 511091 }, 389 | { url = "https://files.pythonhosted.org/packages/ae/c9/4f85f97c9894382ab457382337aea0012711baaa17f2ed55c0ff25f3668a/jiter-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cec323a853c24fd0472517113768c92ae0be8f8c384ef4441d3632da8baa646", size = 492948 }, 390 | { url = "https://files.pythonhosted.org/packages/4d/f2/2e987e0eb465e064c5f52c2f29c8d955452e3b316746e326269263bfb1b7/jiter-0.5.0-cp311-none-win32.whl", hash = "sha256:aa1db0967130b5cab63dfe4d6ff547c88b2a394c3410db64744d491df7f069bb", size = 195183 }, 391 | { url = "https://files.pythonhosted.org/packages/ab/59/05d1c3203c349b37c4dd28b02b9b4e5915a7bcbd9319173b4548a67d2e93/jiter-0.5.0-cp311-none-win_amd64.whl", hash = "sha256:aa9d2b85b2ed7dc7697597dcfaac66e63c1b3028652f751c81c65a9f220899ae", size = 191032 }, 392 | { url = "https://files.pythonhosted.org/packages/aa/bd/c3950e2c478161e131bed8cb67c36aed418190e2a961a1c981e69954e54b/jiter-0.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9f664e7351604f91dcdd557603c57fc0d551bc65cc0a732fdacbf73ad335049a", size = 283511 }, 393 | { url = "https://files.pythonhosted.org/packages/80/1c/8ce58d8c37a589eeaaa5d07d131fd31043886f5e77ab50c00a66d869a361/jiter-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:044f2f1148b5248ad2c8c3afb43430dccf676c5a5834d2f5089a4e6c5bbd64df", size = 296974 }, 394 | { url = "https://files.pythonhosted.org/packages/4d/b8/6faeff9eed8952bed93a77ea1cffae7b946795b88eafd1a60e87a67b09e0/jiter-0.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:702e3520384c88b6e270c55c772d4bd6d7b150608dcc94dea87ceba1b6391248", size = 331897 }, 395 | { url = "https://files.pythonhosted.org/packages/4f/54/1d9a2209b46d39ce6f0cef3ad87c462f9c50312ab84585e6bd5541292b35/jiter-0.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:528d742dcde73fad9d63e8242c036ab4a84389a56e04efd854062b660f559544", size = 342962 }, 396 | { url = "https://files.pythonhosted.org/packages/2a/de/90360be7fc54b2b4c2dfe79eb4ed1f659fce9c96682e6a0be4bbe71371f7/jiter-0.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf80e5fe6ab582c82f0c3331df27a7e1565e2dcf06265afd5173d809cdbf9ba", size = 363844 }, 397 | { url = "https://files.pythonhosted.org/packages/ba/ad/ef32b173191b7a53ea8a6757b80723cba321f8469834825e8c71c96bde17/jiter-0.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44dfc9ddfb9b51a5626568ef4e55ada462b7328996294fe4d36de02fce42721f", size = 378709 }, 398 | { url = "https://files.pythonhosted.org/packages/07/de/353ce53743c0defbbbd652e89c106a97dbbac4eb42c95920b74b5056b93a/jiter-0.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c451f7922992751a936b96c5f5b9bb9312243d9b754c34b33d0cb72c84669f4e", size = 319038 }, 399 | { url = "https://files.pythonhosted.org/packages/3f/92/42d47310bf9530b9dece9e2d7c6d51cf419af5586ededaf5e66622d160e2/jiter-0.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:308fce789a2f093dca1ff91ac391f11a9f99c35369117ad5a5c6c4903e1b3e3a", size = 357763 }, 400 | { url = "https://files.pythonhosted.org/packages/bd/8c/2bb76a9a84474d48fdd133d3445db8a4413da4e87c23879d917e000a9d87/jiter-0.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7f5ad4a7c6b0d90776fdefa294f662e8a86871e601309643de30bf94bb93a64e", size = 511031 }, 401 | { url = "https://files.pythonhosted.org/packages/33/4f/9f23d79c0795e0a8e56e7988e8785c2dcda27e0ed37977256d50c77c6a19/jiter-0.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ea189db75f8eca08807d02ae27929e890c7d47599ce3d0a6a5d41f2419ecf338", size = 493042 }, 402 | { url = "https://files.pythonhosted.org/packages/df/67/8a4f975aa834b8aecdb6b131422390173928fd47f42f269dcc32034ab432/jiter-0.5.0-cp312-none-win32.whl", hash = "sha256:e3bbe3910c724b877846186c25fe3c802e105a2c1fc2b57d6688b9f8772026e4", size = 195405 }, 403 | { url = "https://files.pythonhosted.org/packages/15/81/296b1e25c43db67848728cdab34ac3eb5c5cbb4955ceb3f51ae60d4a5e3d/jiter-0.5.0-cp312-none-win_amd64.whl", hash = "sha256:a586832f70c3f1481732919215f36d41c59ca080fa27a65cf23d9490e75b2ef5", size = 189720 }, 404 | ] 405 | 406 | [[package]] 407 | name = "jmespath" 408 | version = "1.0.1" 409 | source = { registry = "https://pypi.org/simple" } 410 | sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } 411 | wheels = [ 412 | { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, 413 | ] 414 | 415 | [[package]] 416 | name = "jupyter-client" 417 | version = "8.6.2" 418 | source = { registry = "https://pypi.org/simple" } 419 | dependencies = [ 420 | { name = "jupyter-core" }, 421 | { name = "python-dateutil" }, 422 | { name = "pyzmq" }, 423 | { name = "tornado" }, 424 | { name = "traitlets" }, 425 | ] 426 | sdist = { url = "https://files.pythonhosted.org/packages/ff/61/3cd51dea7878691919adc34ff6ad180f13bfe25fb8c7662a9ee6dc64e643/jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df", size = 341102 } 427 | wheels = [ 428 | { url = "https://files.pythonhosted.org/packages/cf/d3/c4bb02580bc0db807edb9a29b2d0c56031be1ef0d804336deb2699a470f6/jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f", size = 105901 }, 429 | ] 430 | 431 | [[package]] 432 | name = "jupyter-core" 433 | version = "5.7.2" 434 | source = { registry = "https://pypi.org/simple" } 435 | dependencies = [ 436 | { name = "platformdirs" }, 437 | { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, 438 | { name = "traitlets" }, 439 | ] 440 | sdist = { url = "https://files.pythonhosted.org/packages/00/11/b56381fa6c3f4cc5d2cf54a7dbf98ad9aa0b339ef7a601d6053538b079a7/jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9", size = 87629 } 441 | wheels = [ 442 | { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 }, 443 | ] 444 | 445 | [[package]] 446 | name = "markupsafe" 447 | version = "2.1.5" 448 | source = { registry = "https://pypi.org/simple" } 449 | sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } 450 | wheels = [ 451 | { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, 452 | { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, 453 | { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, 454 | { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, 455 | { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, 456 | { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, 457 | { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, 458 | { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, 459 | { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, 460 | { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, 461 | { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, 462 | { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, 463 | { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, 464 | { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, 465 | { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, 466 | { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, 467 | { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, 468 | { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, 469 | { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, 470 | { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, 471 | ] 472 | 473 | [[package]] 474 | name = "matplotlib-inline" 475 | version = "0.1.7" 476 | source = { registry = "https://pypi.org/simple" } 477 | dependencies = [ 478 | { name = "traitlets" }, 479 | ] 480 | sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } 481 | wheels = [ 482 | { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, 483 | ] 484 | 485 | [[package]] 486 | name = "nest-asyncio" 487 | version = "1.6.0" 488 | source = { registry = "https://pypi.org/simple" } 489 | sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } 490 | wheels = [ 491 | { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, 492 | ] 493 | 494 | [[package]] 495 | name = "openai" 496 | version = "1.43.0" 497 | source = { registry = "https://pypi.org/simple" } 498 | dependencies = [ 499 | { name = "anyio" }, 500 | { name = "distro" }, 501 | { name = "httpx" }, 502 | { name = "jiter" }, 503 | { name = "pydantic" }, 504 | { name = "sniffio" }, 505 | { name = "tqdm" }, 506 | { name = "typing-extensions" }, 507 | ] 508 | sdist = { url = "https://files.pythonhosted.org/packages/41/80/9390645de4e76bf8195073f23029a9b54cd13b4294e3a5bcb56e4df1aafc/openai-1.43.0.tar.gz", hash = "sha256:e607aff9fc3e28eade107e5edd8ca95a910a4b12589336d3cbb6bfe2ac306b3c", size = 292477 } 509 | wheels = [ 510 | { url = "https://files.pythonhosted.org/packages/5e/4d/affea11bd85ca69d9fdd15567495bb9088ac1c37498c95cb42d9ecd984ed/openai-1.43.0-py3-none-any.whl", hash = "sha256:1a748c2728edd3a738a72a0212ba866f4fdbe39c9ae03813508b267d45104abe", size = 365744 }, 511 | ] 512 | 513 | [[package]] 514 | name = "packaging" 515 | version = "23.2" 516 | source = { registry = "https://pypi.org/simple" } 517 | sdist = { url = "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", size = 146714 } 518 | wheels = [ 519 | { url = "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", size = 53011 }, 520 | ] 521 | 522 | [[package]] 523 | name = "parso" 524 | version = "0.8.4" 525 | source = { registry = "https://pypi.org/simple" } 526 | sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } 527 | wheels = [ 528 | { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, 529 | ] 530 | 531 | [[package]] 532 | name = "pexpect" 533 | version = "4.9.0" 534 | source = { registry = "https://pypi.org/simple" } 535 | dependencies = [ 536 | { name = "ptyprocess" }, 537 | ] 538 | sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } 539 | wheels = [ 540 | { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, 541 | ] 542 | 543 | [[package]] 544 | name = "pillow" 545 | version = "10.4.0" 546 | source = { registry = "https://pypi.org/simple" } 547 | sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059 } 548 | wheels = [ 549 | { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265 }, 550 | { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655 }, 551 | { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304 }, 552 | { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804 }, 553 | { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126 }, 554 | { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541 }, 555 | { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616 }, 556 | { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802 }, 557 | { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213 }, 558 | { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498 }, 559 | { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219 }, 560 | { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350 }, 561 | { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980 }, 562 | { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799 }, 563 | { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973 }, 564 | { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054 }, 565 | { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484 }, 566 | { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375 }, 567 | { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773 }, 568 | { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690 }, 569 | { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951 }, 570 | { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427 }, 571 | { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685 }, 572 | { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883 }, 573 | { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837 }, 574 | { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562 }, 575 | { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761 }, 576 | { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767 }, 577 | { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989 }, 578 | { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255 }, 579 | { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603 }, 580 | { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972 }, 581 | { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375 }, 582 | ] 583 | 584 | [[package]] 585 | name = "platformdirs" 586 | version = "4.2.2" 587 | source = { registry = "https://pypi.org/simple" } 588 | sdist = { url = "https://files.pythonhosted.org/packages/f5/52/0763d1d976d5c262df53ddda8d8d4719eedf9594d046f117c25a27261a19/platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3", size = 20916 } 589 | wheels = [ 590 | { url = "https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", size = 18146 }, 591 | ] 592 | 593 | [[package]] 594 | name = "pluggy" 595 | version = "1.5.0" 596 | source = { registry = "https://pypi.org/simple" } 597 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 598 | wheels = [ 599 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 600 | ] 601 | 602 | [[package]] 603 | name = "prompt-toolkit" 604 | version = "3.0.47" 605 | source = { registry = "https://pypi.org/simple" } 606 | dependencies = [ 607 | { name = "wcwidth" }, 608 | ] 609 | sdist = { url = "https://files.pythonhosted.org/packages/47/6d/0279b119dafc74c1220420028d490c4399b790fc1256998666e3a341879f/prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360", size = 425859 } 610 | wheels = [ 611 | { url = "https://files.pythonhosted.org/packages/e8/23/22750c4b768f09386d1c3cc4337953e8936f48a888fa6dddfb669b2c9088/prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10", size = 386411 }, 612 | ] 613 | 614 | [[package]] 615 | name = "psutil" 616 | version = "6.0.0" 617 | source = { registry = "https://pypi.org/simple" } 618 | sdist = { url = "https://files.pythonhosted.org/packages/18/c7/8c6872f7372eb6a6b2e4708b88419fb46b857f7a2e1892966b851cc79fc9/psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2", size = 508067 } 619 | wheels = [ 620 | { url = "https://files.pythonhosted.org/packages/c5/66/78c9c3020f573c58101dc43a44f6855d01bbbd747e24da2f0c4491200ea3/psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35", size = 249766 }, 621 | { url = "https://files.pythonhosted.org/packages/e1/3f/2403aa9558bea4d3854b0e5e567bc3dd8e9fbc1fc4453c0aa9aafeb75467/psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1", size = 253024 }, 622 | { url = "https://files.pythonhosted.org/packages/0b/37/f8da2fbd29690b3557cca414c1949f92162981920699cd62095a984983bf/psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0", size = 250961 }, 623 | { url = "https://files.pythonhosted.org/packages/35/56/72f86175e81c656a01c4401cd3b1c923f891b31fbcebe98985894176d7c9/psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0", size = 287478 }, 624 | { url = "https://files.pythonhosted.org/packages/19/74/f59e7e0d392bc1070e9a70e2f9190d652487ac115bb16e2eff6b22ad1d24/psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd", size = 290455 }, 625 | { url = "https://files.pythonhosted.org/packages/cd/5f/60038e277ff0a9cc8f0c9ea3d0c5eb6ee1d2470ea3f9389d776432888e47/psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132", size = 292046 }, 626 | { url = "https://files.pythonhosted.org/packages/8b/20/2ff69ad9c35c3df1858ac4e094f20bd2374d33c8643cf41da8fd7cdcb78b/psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d", size = 253560 }, 627 | { url = "https://files.pythonhosted.org/packages/73/44/561092313ae925f3acfaace6f9ddc4f6a9c748704317bad9c8c8f8a36a79/psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3", size = 257399 }, 628 | { url = "https://files.pythonhosted.org/packages/7c/06/63872a64c312a24fb9b4af123ee7007a306617da63ff13bcc1432386ead7/psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0", size = 251988 }, 629 | ] 630 | 631 | [[package]] 632 | name = "ptyprocess" 633 | version = "0.7.0" 634 | source = { registry = "https://pypi.org/simple" } 635 | sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } 636 | wheels = [ 637 | { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, 638 | ] 639 | 640 | [[package]] 641 | name = "pure-eval" 642 | version = "0.2.3" 643 | source = { registry = "https://pypi.org/simple" } 644 | sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } 645 | wheels = [ 646 | { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, 647 | ] 648 | 649 | [[package]] 650 | name = "pycparser" 651 | version = "2.22" 652 | source = { registry = "https://pypi.org/simple" } 653 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } 654 | wheels = [ 655 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, 656 | ] 657 | 658 | [[package]] 659 | name = "pydantic" 660 | version = "2.8.2" 661 | source = { registry = "https://pypi.org/simple" } 662 | dependencies = [ 663 | { name = "annotated-types" }, 664 | { name = "pydantic-core" }, 665 | { name = "typing-extensions" }, 666 | ] 667 | sdist = { url = "https://files.pythonhosted.org/packages/8c/99/d0a5dca411e0a017762258013ba9905cd6e7baa9a3fd1fe8b6529472902e/pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a", size = 739834 } 668 | wheels = [ 669 | { url = "https://files.pythonhosted.org/packages/1f/fa/b7f815b8c9ad021c07f88875b601222ef5e70619391ade4a49234d12d278/pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8", size = 423875 }, 670 | ] 671 | 672 | [[package]] 673 | name = "pydantic-core" 674 | version = "2.20.1" 675 | source = { registry = "https://pypi.org/simple" } 676 | dependencies = [ 677 | { name = "typing-extensions" }, 678 | ] 679 | sdist = { url = "https://files.pythonhosted.org/packages/12/e3/0d5ad91211dba310f7ded335f4dad871172b9cc9ce204f5a56d76ccd6247/pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4", size = 388371 } 680 | wheels = [ 681 | { url = "https://files.pythonhosted.org/packages/61/db/f6a724db226d990a329910727cfac43539ff6969edc217286dd05cda3ef6/pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312", size = 1834507 }, 682 | { url = "https://files.pythonhosted.org/packages/9b/83/6f2bfe75209d557ae1c3550c1252684fc1827b8b12fbed84c3b4439e135d/pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88", size = 1773527 }, 683 | { url = "https://files.pythonhosted.org/packages/93/ef/513ea76d7ca81f2354bb9c8d7839fc1157673e652613f7e1aff17d8ce05d/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc", size = 1787879 }, 684 | { url = "https://files.pythonhosted.org/packages/31/0a/ac294caecf235f0cc651de6232f1642bb793af448d1cfc541b0dc1fd72b8/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43", size = 1774694 }, 685 | { url = "https://files.pythonhosted.org/packages/46/a4/08f12b5512f095963550a7cb49ae010e3f8f3f22b45e508c2cb4d7744fce/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6", size = 1976369 }, 686 | { url = "https://files.pythonhosted.org/packages/15/59/b2495be4410462aedb399071c71884042a2c6443319cbf62d00b4a7ed7a5/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121", size = 2691250 }, 687 | { url = "https://files.pythonhosted.org/packages/3c/ae/fc99ce1ba791c9e9d1dee04ce80eef1dae5b25b27e3fc8e19f4e3f1348bf/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1", size = 2061462 }, 688 | { url = "https://files.pythonhosted.org/packages/44/bb/eb07cbe47cfd638603ce3cb8c220f1a054b821e666509e535f27ba07ca5f/pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b", size = 1893923 }, 689 | { url = "https://files.pythonhosted.org/packages/ce/ef/5a52400553b8faa0e7f11fd7a2ba11e8d2feb50b540f9e7973c49b97eac0/pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27", size = 1966779 }, 690 | { url = "https://files.pythonhosted.org/packages/4c/5b/fb37fe341344d9651f5c5f579639cd97d50a457dc53901aa8f7e9f28beb9/pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b", size = 2109044 }, 691 | { url = "https://files.pythonhosted.org/packages/70/1a/6f7278802dbc66716661618807ab0dfa4fc32b09d1235923bbbe8b3a5757/pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a", size = 1708265 }, 692 | { url = "https://files.pythonhosted.org/packages/35/7f/58758c42c61b0bdd585158586fecea295523d49933cb33664ea888162daf/pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2", size = 1901750 }, 693 | { url = "https://files.pythonhosted.org/packages/6f/47/ef0d60ae23c41aced42921728650460dc831a0adf604bfa66b76028cb4d0/pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231", size = 1839225 }, 694 | { url = "https://files.pythonhosted.org/packages/6a/23/430f2878c9cd977a61bb39f71751d9310ec55cee36b3d5bf1752c6341fd0/pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9", size = 1768604 }, 695 | { url = "https://files.pythonhosted.org/packages/9e/2b/ec4e7225dee79e0dc80ccc3c35ab33cc2c4bbb8a1a7ecf060e5e453651ec/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f", size = 1789767 }, 696 | { url = "https://files.pythonhosted.org/packages/64/b0/38b24a1fa6d2f96af3148362e10737ec073768cd44d3ec21dca3be40a519/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52", size = 1772061 }, 697 | { url = "https://files.pythonhosted.org/packages/5e/da/bb73274c42cb60decfa61e9eb0c9029da78b3b9af0a9de0309dbc8ff87b6/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237", size = 1974573 }, 698 | { url = "https://files.pythonhosted.org/packages/c8/65/41693110fb3552556180460daffdb8bbeefb87fc026fd9aa4b849374015c/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe", size = 2625596 }, 699 | { url = "https://files.pythonhosted.org/packages/09/b3/a5a54b47cccd1ab661ed5775235c5e06924753c2d4817737c5667bfa19a8/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e", size = 2099064 }, 700 | { url = "https://files.pythonhosted.org/packages/52/fa/443a7a6ea54beaba45ff3a59f3d3e6e3004b7460bcfb0be77bcf98719d3b/pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24", size = 1900345 }, 701 | { url = "https://files.pythonhosted.org/packages/8e/e6/9aca9ffae60f9cdf0183069de3e271889b628d0fb175913fcb3db5618fb1/pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1", size = 1968252 }, 702 | { url = "https://files.pythonhosted.org/packages/46/5e/6c716810ea20a6419188992973a73c2fb4eb99cd382368d0637ddb6d3c99/pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd", size = 2119191 }, 703 | { url = "https://files.pythonhosted.org/packages/06/fc/6123b00a9240fbb9ae0babad7a005d51103d9a5d39c957a986f5cdd0c271/pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688", size = 1717788 }, 704 | { url = "https://files.pythonhosted.org/packages/d5/36/e61ad5a46607a469e2786f398cd671ebafcd9fb17f09a2359985c7228df5/pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d", size = 1898188 }, 705 | { url = "https://files.pythonhosted.org/packages/49/75/40b0e98b658fdba02a693b3bacb4c875a28bba87796c7b13975976597d8c/pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686", size = 1838688 }, 706 | { url = "https://files.pythonhosted.org/packages/75/02/d8ba2d4a266591a6a623c68b331b96523d4b62ab82a951794e3ed8907390/pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a", size = 1768409 }, 707 | { url = "https://files.pythonhosted.org/packages/91/ae/25ecd9bc4ce4993e99a1a3c9ab111c082630c914260e129572fafed4ecc2/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b", size = 1789317 }, 708 | { url = "https://files.pythonhosted.org/packages/7a/80/72057580681cdbe55699c367963d9c661b569a1d39338b4f6239faf36cdc/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19", size = 1771949 }, 709 | { url = "https://files.pythonhosted.org/packages/a2/be/d9bbabc55b05019013180f141fcaf3b14dbe15ca7da550e95b60c321009a/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac", size = 1974392 }, 710 | { url = "https://files.pythonhosted.org/packages/79/2d/7bcd938c6afb0f40293283f5f09988b61fb0a4f1d180abe7c23a2f665f8e/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703", size = 2625565 }, 711 | { url = "https://files.pythonhosted.org/packages/ac/88/ca758e979457096008a4b16a064509028e3e092a1e85a5ed6c18ced8da88/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c", size = 2098784 }, 712 | { url = "https://files.pythonhosted.org/packages/eb/de/2fad6d63c3c42e472e985acb12ec45b7f56e42e6f4cd6dfbc5e87ee8678c/pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83", size = 1900198 }, 713 | { url = "https://files.pythonhosted.org/packages/fe/50/077c7f35b6488dc369a6d22993af3a37901e198630f38ac43391ca730f5b/pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203", size = 1968005 }, 714 | { url = "https://files.pythonhosted.org/packages/5d/1f/f378631574ead46d636b9a04a80ff878b9365d4b361b1905ef1667d4182a/pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0", size = 2118920 }, 715 | { url = "https://files.pythonhosted.org/packages/7a/ea/e4943f17df7a3031d709481fe4363d4624ae875a6409aec34c28c9e6cf59/pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e", size = 1717397 }, 716 | { url = "https://files.pythonhosted.org/packages/13/63/b95781763e8d84207025071c0cec16d921c0163c7a9033ae4b9a0e020dc7/pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20", size = 1898013 }, 717 | ] 718 | 719 | [[package]] 720 | name = "pygments" 721 | version = "2.18.0" 722 | source = { registry = "https://pypi.org/simple" } 723 | sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } 724 | wheels = [ 725 | { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, 726 | ] 727 | 728 | [[package]] 729 | name = "pymupdf" 730 | version = "1.22.5" 731 | source = { registry = "https://pypi.org/simple" } 732 | sdist = { url = "https://files.pythonhosted.org/packages/f6/6a/199e6b76f1cca112510171df0949af1fcf43536812441866e7c9e1d7b01e/PyMuPDF-1.22.5.tar.gz", hash = "sha256:5ec8d5106752297529d0d68d46cfc4ce99914aabd99be843f1599a1842d63fe9", size = 61638053 } 733 | wheels = [ 734 | { url = "https://files.pythonhosted.org/packages/05/cf/c1f221571dd1c43baaea9e0c27797c88406af7eadedc09876682d9ce857c/PyMuPDF-1.22.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:017aaba511526facfc928e9d95d2c10d28a2821b05b9039bf422031a7da8584e", size = 12906555 }, 735 | { url = "https://files.pythonhosted.org/packages/b5/f7/016c23f678b96821f8708a7b8db67dc784d8ef635668188b9864246a7754/PyMuPDF-1.22.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6fe5e44a14864d921fb96669a82f9635846806176f77f1d73c61feb84ebf4d84", size = 12667247 }, 736 | { url = "https://files.pythonhosted.org/packages/7b/05/e8eb60448483b6ba7deea936a8cc0e98be9120521a3bdac7371b718ecca8/PyMuPDF-1.22.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e74d766f79e41e10c51865233042ab2cc4612ca7942812dca0603f4d0f8f73d", size = 14092342 }, 737 | { url = "https://files.pythonhosted.org/packages/c2/ce/d9b515a5533da2d917f3eae2286b394fd9d813178360e374374f63844b65/PyMuPDF-1.22.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8175452fcc99a0af6429d8acd87682a3a70c5879d73532c7327f71ce508a35", size = 14195073 }, 738 | { url = "https://files.pythonhosted.org/packages/6f/bb/62d17402feb10babe33955b7b17967c7a83abd102aa6fac49893e10a8e48/PyMuPDF-1.22.5-cp311-cp311-win32.whl", hash = "sha256:42f59f4999d7f8b35c850050bd965e98c081a7d9b92d5f9dcf30203b30d06876", size = 11014088 }, 739 | { url = "https://files.pythonhosted.org/packages/89/7c/e9d2259ec46547bfe4956fde5c4e42c1421acd20c4bcf2da772b4888bb6f/PyMuPDF-1.22.5-cp311-cp311-win_amd64.whl", hash = "sha256:3d71c47aa14b73f2df7d03be8c547a05df6c6898d8c63a0f752b26f206eefd3c", size = 11752116 }, 740 | ] 741 | 742 | [[package]] 743 | name = "pytesseract" 744 | version = "0.3.13" 745 | source = { registry = "https://pypi.org/simple" } 746 | dependencies = [ 747 | { name = "packaging" }, 748 | { name = "pillow" }, 749 | ] 750 | sdist = { url = "https://files.pythonhosted.org/packages/9f/a6/7d679b83c285974a7cb94d739b461fa7e7a9b17a3abfd7bf6cbc5c2394b0/pytesseract-0.3.13.tar.gz", hash = "sha256:4bf5f880c99406f52a3cfc2633e42d9dc67615e69d8a509d74867d3baddb5db9", size = 17689 } 751 | wheels = [ 752 | { url = "https://files.pythonhosted.org/packages/7a/33/8312d7ce74670c9d39a532b2c246a853861120486be9443eebf048043637/pytesseract-0.3.13-py3-none-any.whl", hash = "sha256:7a99c6c2ac598360693d83a416e36e0b33a67638bb9d77fdcac094a3589d4b34", size = 14705 }, 753 | ] 754 | 755 | [[package]] 756 | name = "pytest" 757 | version = "7.4.4" 758 | source = { registry = "https://pypi.org/simple" } 759 | dependencies = [ 760 | { name = "colorama", marker = "sys_platform == 'win32'" }, 761 | { name = "iniconfig" }, 762 | { name = "packaging" }, 763 | { name = "pluggy" }, 764 | ] 765 | sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116 } 766 | wheels = [ 767 | { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287 }, 768 | ] 769 | 770 | [[package]] 771 | name = "python-dateutil" 772 | version = "2.9.0.post0" 773 | source = { registry = "https://pypi.org/simple" } 774 | dependencies = [ 775 | { name = "six" }, 776 | ] 777 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 778 | wheels = [ 779 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, 780 | ] 781 | 782 | [[package]] 783 | name = "pywin32" 784 | version = "306" 785 | source = { registry = "https://pypi.org/simple" } 786 | wheels = [ 787 | { url = "https://files.pythonhosted.org/packages/8b/1e/fc18ad83ca553e01b97aa8393ff10e33c1fb57801db05488b83282ee9913/pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", size = 8507689 }, 788 | { url = "https://files.pythonhosted.org/packages/7e/9e/ad6b1ae2a5ad1066dc509350e0fbf74d8d50251a51e420a2a8feaa0cecbd/pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", size = 9227547 }, 789 | { url = "https://files.pythonhosted.org/packages/91/20/f744bff1da8f43388498503634378dbbefbe493e65675f2cc52f7185c2c2/pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", size = 10388324 }, 790 | { url = "https://files.pythonhosted.org/packages/14/91/17e016d5923e178346aabda3dfec6629d1a26efe587d19667542105cf0a6/pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", size = 8507705 }, 791 | { url = "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", size = 9227429 }, 792 | { url = "https://files.pythonhosted.org/packages/1c/43/e3444dc9a12f8365d9603c2145d16bf0a2f8180f343cf87be47f5579e547/pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", size = 10388145 }, 793 | ] 794 | 795 | [[package]] 796 | name = "pyyaml" 797 | version = "6.0.2" 798 | source = { registry = "https://pypi.org/simple" } 799 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 800 | wheels = [ 801 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, 802 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, 803 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, 804 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, 805 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, 806 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, 807 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, 808 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, 809 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, 810 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, 811 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, 812 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, 813 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, 814 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, 815 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, 816 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, 817 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, 818 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, 819 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, 820 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, 821 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, 822 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, 823 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, 824 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, 825 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, 826 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 827 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 828 | ] 829 | 830 | [[package]] 831 | name = "pyzmq" 832 | version = "26.2.0" 833 | source = { registry = "https://pypi.org/simple" } 834 | dependencies = [ 835 | { name = "cffi", marker = "implementation_name == 'pypy'" }, 836 | ] 837 | sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 } 838 | wheels = [ 839 | { url = "https://files.pythonhosted.org/packages/12/20/de7442172f77f7c96299a0ac70e7d4fb78cd51eca67aa2cf552b66c14196/pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218", size = 1340639 }, 840 | { url = "https://files.pythonhosted.org/packages/98/4d/5000468bd64c7910190ed0a6c76a1ca59a68189ec1f007c451dc181a22f4/pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4", size = 1008710 }, 841 | { url = "https://files.pythonhosted.org/packages/e1/bf/c67fd638c2f9fbbab8090a3ee779370b97c82b84cc12d0c498b285d7b2c0/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef", size = 673129 }, 842 | { url = "https://files.pythonhosted.org/packages/86/94/99085a3f492aa538161cbf27246e8886ff850e113e0c294a5b8245f13b52/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317", size = 910107 }, 843 | { url = "https://files.pythonhosted.org/packages/31/1d/346809e8a9b999646d03f21096428453465b1bca5cd5c64ecd048d9ecb01/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf", size = 867960 }, 844 | { url = "https://files.pythonhosted.org/packages/ab/68/6fb6ae5551846ad5beca295b7bca32bf0a7ce19f135cb30e55fa2314e6b6/pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e", size = 869204 }, 845 | { url = "https://files.pythonhosted.org/packages/0f/f9/18417771dee223ccf0f48e29adf8b4e25ba6d0e8285e33bcbce078070bc3/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37", size = 1203351 }, 846 | { url = "https://files.pythonhosted.org/packages/e0/46/f13e67fe0d4f8a2315782cbad50493de6203ea0d744610faf4d5f5b16e90/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3", size = 1514204 }, 847 | { url = "https://files.pythonhosted.org/packages/50/11/ddcf7343b7b7a226e0fc7b68cbf5a5bb56291fac07f5c3023bb4c319ebb4/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6", size = 1414339 }, 848 | { url = "https://files.pythonhosted.org/packages/01/14/1c18d7d5b7be2708f513f37c61bfadfa62161c10624f8733f1c8451b3509/pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4", size = 576928 }, 849 | { url = "https://files.pythonhosted.org/packages/3b/1b/0a540edd75a41df14ec416a9a500b9fec66e554aac920d4c58fbd5756776/pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5", size = 642317 }, 850 | { url = "https://files.pythonhosted.org/packages/98/77/1cbfec0358078a4c5add529d8a70892db1be900980cdb5dd0898b3d6ab9d/pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003", size = 543834 }, 851 | { url = "https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", size = 1343105 }, 852 | { url = "https://files.pythonhosted.org/packages/b7/9c/4b1e2d3d4065be715e007fe063ec7885978fad285f87eae1436e6c3201f4/pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", size = 1008365 }, 853 | { url = "https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", size = 665923 }, 854 | { url = "https://files.pythonhosted.org/packages/ae/61/d436461a47437d63c6302c90724cf0981883ec57ceb6073873f32172d676/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", size = 903400 }, 855 | { url = "https://files.pythonhosted.org/packages/47/42/fc6d35ecefe1739a819afaf6f8e686f7f02a4dd241c78972d316f403474c/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", size = 860034 }, 856 | { url = "https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", size = 860579 }, 857 | { url = "https://files.pythonhosted.org/packages/38/6f/4df2014ab553a6052b0e551b37da55166991510f9e1002c89cab7ce3b3f2/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", size = 1196246 }, 858 | { url = "https://files.pythonhosted.org/packages/38/9d/ee240fc0c9fe9817f0c9127a43238a3e28048795483c403cc10720ddef22/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", size = 1507441 }, 859 | { url = "https://files.pythonhosted.org/packages/85/4f/01711edaa58d535eac4a26c294c617c9a01f09857c0ce191fd574d06f359/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", size = 1406498 }, 860 | { url = "https://files.pythonhosted.org/packages/07/18/907134c85c7152f679ed744e73e645b365f3ad571f38bdb62e36f347699a/pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7", size = 575533 }, 861 | { url = "https://files.pythonhosted.org/packages/ce/2c/a6f4a20202a4d3c582ad93f95ee78d79bbdc26803495aec2912b17dbbb6c/pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a", size = 637768 }, 862 | { url = "https://files.pythonhosted.org/packages/5f/0e/eb16ff731632d30554bf5af4dbba3ffcd04518219d82028aea4ae1b02ca5/pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b", size = 540675 }, 863 | { url = "https://files.pythonhosted.org/packages/04/a7/0f7e2f6c126fe6e62dbae0bc93b1bd3f1099cf7fea47a5468defebe3f39d/pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726", size = 1006564 }, 864 | { url = "https://files.pythonhosted.org/packages/31/b6/a187165c852c5d49f826a690857684333a6a4a065af0a6015572d2284f6a/pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3", size = 1340447 }, 865 | { url = "https://files.pythonhosted.org/packages/68/ba/f4280c58ff71f321602a6e24fd19879b7e79793fb8ab14027027c0fb58ef/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50", size = 665485 }, 866 | { url = "https://files.pythonhosted.org/packages/77/b5/c987a5c53c7d8704216f29fc3d810b32f156bcea488a940e330e1bcbb88d/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb", size = 903484 }, 867 | { url = "https://files.pythonhosted.org/packages/29/c9/07da157d2db18c72a7eccef8e684cefc155b712a88e3d479d930aa9eceba/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187", size = 859981 }, 868 | { url = "https://files.pythonhosted.org/packages/43/09/e12501bd0b8394b7d02c41efd35c537a1988da67fc9c745cae9c6c776d31/pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b", size = 860334 }, 869 | { url = "https://files.pythonhosted.org/packages/eb/ff/f5ec1d455f8f7385cc0a8b2acd8c807d7fade875c14c44b85c1bddabae21/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18", size = 1196179 }, 870 | { url = "https://files.pythonhosted.org/packages/ec/8a/bb2ac43295b1950fe436a81fc5b298be0b96ac76fb029b514d3ed58f7b27/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115", size = 1507668 }, 871 | { url = "https://files.pythonhosted.org/packages/a9/49/dbc284ebcfd2dca23f6349227ff1616a7ee2c4a35fe0a5d6c3deff2b4fed/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e", size = 1406539 }, 872 | { url = "https://files.pythonhosted.org/packages/00/68/093cdce3fe31e30a341d8e52a1ad86392e13c57970d722c1f62a1d1a54b6/pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5", size = 575567 }, 873 | { url = "https://files.pythonhosted.org/packages/92/ae/6cc4657148143412b5819b05e362ae7dd09fb9fe76e2a539dcff3d0386bc/pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad", size = 637551 }, 874 | { url = "https://files.pythonhosted.org/packages/6c/67/fbff102e201688f97c8092e4c3445d1c1068c2f27bbd45a578df97ed5f94/pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797", size = 540378 }, 875 | { url = "https://files.pythonhosted.org/packages/3f/fe/2d998380b6e0122c6c4bdf9b6caf490831e5f5e2d08a203b5adff060c226/pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a", size = 1007378 }, 876 | { url = "https://files.pythonhosted.org/packages/4a/f4/30d6e7157f12b3a0390bde94d6a8567cdb88846ed068a6e17238a4ccf600/pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc", size = 1329532 }, 877 | { url = "https://files.pythonhosted.org/packages/82/86/3fe917870e15ee1c3ad48229a2a64458e36036e64b4afa9659045d82bfa8/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5", size = 653242 }, 878 | { url = "https://files.pythonhosted.org/packages/50/2d/242e7e6ef6c8c19e6cb52d095834508cd581ffb925699fd3c640cdc758f1/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672", size = 888404 }, 879 | { url = "https://files.pythonhosted.org/packages/ac/11/7270566e1f31e4ea73c81ec821a4b1688fd551009a3d2bab11ec66cb1e8f/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797", size = 845858 }, 880 | { url = "https://files.pythonhosted.org/packages/91/d5/72b38fbc69867795c8711bdd735312f9fef1e3d9204e2f63ab57085434b9/pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386", size = 847375 }, 881 | { url = "https://files.pythonhosted.org/packages/dd/9a/10ed3c7f72b4c24e719c59359fbadd1a27556a28b36cdf1cd9e4fb7845d5/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", size = 1183489 }, 882 | { url = "https://files.pythonhosted.org/packages/72/2d/8660892543fabf1fe41861efa222455811adac9f3c0818d6c3170a1153e3/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", size = 1492932 }, 883 | { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 }, 884 | ] 885 | 886 | [[package]] 887 | name = "redbox" 888 | version = "0.2.1" 889 | source = { registry = "https://pypi.org/simple" } 890 | dependencies = [ 891 | { name = "pydantic" }, 892 | ] 893 | sdist = { url = "https://files.pythonhosted.org/packages/5b/33/42dbfd394d8099079d31dc0f98afca62bc6cc9635ea1ccab1029fefdc6ff/redbox-0.2.1.tar.gz", hash = "sha256:17005f8cfe8acba992b649e5682b2dd4bff937d67df3fd8496e187cae4f19d60", size = 219953 } 894 | wheels = [ 895 | { url = "https://files.pythonhosted.org/packages/24/24/9f8330b5ce5a64cd97ae2d3a00d1d5cb9096c54ac2e56a05d7a5812709b8/redbox-0.2.1-py3-none-any.whl", hash = "sha256:14906668345c7e76db367d6d40347c2dcb5de2a5167f96d08f06f95c0a908f71", size = 16507 }, 896 | ] 897 | 898 | [[package]] 899 | name = "redmail" 900 | version = "0.6.0" 901 | source = { registry = "https://pypi.org/simple" } 902 | dependencies = [ 903 | { name = "jinja2" }, 904 | ] 905 | sdist = { url = "https://files.pythonhosted.org/packages/e9/96/36c740474cadc1b8a6e735334a0c67c02ea7169d29ffde48eb6c74f3abaa/redmail-0.6.0.tar.gz", hash = "sha256:0447cbd76deb2788b2d831c12e22b513587e99f725071d9951a01b0f2b8d0a72", size = 448832 } 906 | wheels = [ 907 | { url = "https://files.pythonhosted.org/packages/83/67/3e0005b255a9d02448c5529af450b6807403e9af7b82636123273906ea37/redmail-0.6.0-py3-none-any.whl", hash = "sha256:8e64a680ffc8aaf8054312bf8b216da8fed20669181b77b1f1ccbdf4ee064427", size = 46948 }, 908 | ] 909 | 910 | [[package]] 911 | name = "remarks" 912 | version = "0.3.10" 913 | source = { git = "https://github.com/azeirah/remarks?branch=v6_with_rmscene#34952c345cb2575184b80e2b5fbb6318a31c20fc" } 914 | dependencies = [ 915 | { name = "pymupdf" }, 916 | { name = "pytest" }, 917 | { name = "pyyaml" }, 918 | { name = "rmscene" }, 919 | { name = "shapely" }, 920 | { name = "syrupy" }, 921 | ] 922 | 923 | [[package]] 924 | name = "rmscene" 925 | version = "0.5.0" 926 | source = { git = "https://github.com/ricklupton/rmscene?rev=fbab6274ed8ca29f9a9bf4fd36b6fa20cc977a1f#fbab6274ed8ca29f9a9bf4fd36b6fa20cc977a1f" } 927 | dependencies = [ 928 | { name = "packaging" }, 929 | ] 930 | 931 | [[package]] 932 | name = "ruff" 933 | version = "0.6.3" 934 | source = { registry = "https://pypi.org/simple" } 935 | sdist = { url = "https://files.pythonhosted.org/packages/5d/f9/0b32e5d1c6f957df49398cd882a011e9488fcbca0d6acfeeea50ccd37a4d/ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983", size = 2463514 } 936 | wheels = [ 937 | { url = "https://files.pythonhosted.org/packages/72/68/1da6a1e39a03a229ea57c511691d6225072759cc7764206c3f0989521194/ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3", size = 9696928 }, 938 | { url = "https://files.pythonhosted.org/packages/6e/59/3b8b1d3a4271c6eb6ceecd3cef19a6d881639a0f18ad651563d6f619aaae/ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc", size = 9448462 }, 939 | { url = "https://files.pythonhosted.org/packages/35/4f/b942ecb8bbebe53aa9b33e9b96df88acd50b70adaaed3070f1d92131a1cb/ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1", size = 9176190 }, 940 | { url = "https://files.pythonhosted.org/packages/a0/20/b0bcb29d4ee437f3567b73b6905c034e2e94d29b9b826c66daecc1cf6388/ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1", size = 10108892 }, 941 | { url = "https://files.pythonhosted.org/packages/9c/e3/211bc759f424e8823a9937e0f678695ca02113c621dfde1fa756f9f26f6d/ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672", size = 9476471 }, 942 | { url = "https://files.pythonhosted.org/packages/b2/a3/2ec35a2d7a554364864206f0e46812b92a074ad8a014b923d821ead532aa/ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1", size = 10294802 }, 943 | { url = "https://files.pythonhosted.org/packages/03/8b/56ef687b3489c88886dea48c78fb4969b6b65f18007d0ac450070edd1f58/ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384", size = 11022372 }, 944 | { url = "https://files.pythonhosted.org/packages/a5/21/327d147feb442adb88975e81e2263102789eba9ad2afa102c661912a482f/ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a", size = 10596596 }, 945 | { url = "https://files.pythonhosted.org/packages/6c/86/ff386de63729da3e08c8099c57f577a00ec9f3eea711b23ac07cf3588dc5/ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500", size = 11572830 }, 946 | { url = "https://files.pythonhosted.org/packages/38/5d/b33284c108e3f315ddd09b70296fd76bd28ecf8965a520bc93f3bbd8ac40/ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470", size = 10262577 }, 947 | { url = "https://files.pythonhosted.org/packages/29/99/9cdfad0d7f460e66567236eddc691473791afd9aff93a0dfcdef0462a6c7/ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f", size = 10098751 }, 948 | { url = "https://files.pythonhosted.org/packages/a8/9f/f801a1619f5549e552f1f722f1db57eb39e7e1d83d482133142781d450de/ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5", size = 9563859 }, 949 | { url = "https://files.pythonhosted.org/packages/0b/4d/fb2424faf04ffdb960ae2b3a1d991c5183dd981003de727d2d5cc38abc98/ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351", size = 9914291 }, 950 | { url = "https://files.pythonhosted.org/packages/2e/dd/94fddf002a8f6152e8ebfbb51d3f93febc415c1fe694345623c31ce8b33b/ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8", size = 10331549 }, 951 | { url = "https://files.pythonhosted.org/packages/b4/73/ca9c2f9237a430ca423b6dca83b77e9a428afeb7aec80596e86c369123fe/ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521", size = 7962163 }, 952 | { url = "https://files.pythonhosted.org/packages/55/ce/061c605b1dfb52748d59bc0c7a8507546c178801156415773d18febfd71d/ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb", size = 8800901 }, 953 | { url = "https://files.pythonhosted.org/packages/63/28/ae4ffe7d3b6134ca6d31ebef07447ef70097c4a9e8fbbc519b374c5c1559/ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82", size = 8229171 }, 954 | ] 955 | 956 | [[package]] 957 | name = "s3transfer" 958 | version = "0.10.2" 959 | source = { registry = "https://pypi.org/simple" } 960 | dependencies = [ 961 | { name = "botocore" }, 962 | ] 963 | sdist = { url = "https://files.pythonhosted.org/packages/cb/67/94c6730ee4c34505b14d94040e2f31edf144c230b6b49e971b4f25ff8fab/s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", size = 144095 } 964 | wheels = [ 965 | { url = "https://files.pythonhosted.org/packages/3c/4a/b221409913760d26cf4498b7b1741d510c82d3ad38381984a3ddc135ec66/s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69", size = 82716 }, 966 | ] 967 | 968 | [[package]] 969 | name = "shapely" 970 | version = "1.8.5.post1" 971 | source = { registry = "https://pypi.org/simple" } 972 | sdist = { url = "https://files.pythonhosted.org/packages/92/2e/a8bbe3c6b414c3c61c4b639ab16d5b1f9c4c4095817d417b503413e613c0/Shapely-1.8.5.post1.tar.gz", hash = "sha256:ef3be705c3eac282a28058e6c6e5503419b250f482320df2172abcbea642c831", size = 200928 } 973 | wheels = [ 974 | { url = "https://files.pythonhosted.org/packages/3e/0d/29f313b99579e0c54d2b09a60c8e7f71382a44dd725139dbbce51933868a/Shapely-1.8.5.post1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:783bad5f48e2708a0e2f695a34ed382e4162c795cb2f0368b39528ac1d6db7ed", size = 2164063 }, 975 | { url = "https://files.pythonhosted.org/packages/55/95/f694322ba2dc37f7956dbf1bfb924ac42979b04c65b5f37631726729acba/Shapely-1.8.5.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a23ef3882d6aa203dd3623a3d55d698f59bfbd9f8a3bfed52c2da05a7f0f8640", size = 1201426 }, 976 | { url = "https://files.pythonhosted.org/packages/24/97/940c9f7c0bc20ca2121ca9fdb4cbf83e5239f672bcd02d3be486e5e3f012/Shapely-1.8.5.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab38f7b5196ace05725e407cb8cab9ff66edb8e6f7bb36a398e8f73f52a7aaa2", size = 1056728 }, 977 | { url = "https://files.pythonhosted.org/packages/04/65/bc86d90b3cdf99e9851403f0b1558b46eb51217d6786cc5e1ccb22ea5211/Shapely-1.8.5.post1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d086591f744be483b34628b391d741e46f2645fe37594319e0a673cc2c26bcf", size = 2326486 }, 978 | { url = "https://files.pythonhosted.org/packages/d7/8e/68d8278ab04c89dc14b1b1c2bf95b1df18a9b461c6b5f85649f3602d267e/Shapely-1.8.5.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4728666fff8cccc65a07448cae72c75a8773fea061c3f4f139c44adc429b18c3", size = 2162106 }, 979 | { url = "https://files.pythonhosted.org/packages/95/c4/f762a599d63d433012745b3c893a355dabf01e2fd5080c4c60f383ac47f5/Shapely-1.8.5.post1-cp311-cp311-win32.whl", hash = "sha256:84010db15eb364a52b74ea8804ef92a6a930dfc1981d17a369444b6ddec66efd", size = 1170996 }, 980 | { url = "https://files.pythonhosted.org/packages/56/bf/49e91fcad7bbe562141d6337a7f3c7698da513c9f07264b2c02150cf590a/Shapely-1.8.5.post1-cp311-cp311-win_amd64.whl", hash = "sha256:48dcfffb9e225c0481120f4bdf622131c8c95f342b00b158cdbe220edbbe20b6", size = 1299051 }, 981 | ] 982 | 983 | [[package]] 984 | name = "six" 985 | version = "1.12.0" 986 | source = { registry = "https://pypi.org/simple" } 987 | sdist = { url = "https://files.pythonhosted.org/packages/dd/bf/4138e7bfb757de47d1f4b6994648ec67a51efe58fa907c1e11e350cddfca/six-1.12.0.tar.gz", hash = "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73", size = 32725 } 988 | wheels = [ 989 | { url = "https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl", hash = "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", size = 10586 }, 990 | ] 991 | 992 | [[package]] 993 | name = "sniffio" 994 | version = "1.3.1" 995 | source = { registry = "https://pypi.org/simple" } 996 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 997 | wheels = [ 998 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 999 | ] 1000 | 1001 | [[package]] 1002 | name = "stack-data" 1003 | version = "0.6.3" 1004 | source = { registry = "https://pypi.org/simple" } 1005 | dependencies = [ 1006 | { name = "asttokens" }, 1007 | { name = "executing" }, 1008 | { name = "pure-eval" }, 1009 | ] 1010 | sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } 1011 | wheels = [ 1012 | { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "syrupy" 1017 | version = "4.7.1" 1018 | source = { registry = "https://pypi.org/simple" } 1019 | dependencies = [ 1020 | { name = "pytest" }, 1021 | ] 1022 | sdist = { url = "https://files.pythonhosted.org/packages/db/ac/105c151335bf71ddf7f3c77118438cad77d4cf092559a6b429bca1bb436b/syrupy-4.7.1.tar.gz", hash = "sha256:f9d4485f3f27d0e5df6ed299cac6fa32eb40a441915d988e82be5a4bdda335c8", size = 49117 } 1023 | wheels = [ 1024 | { url = "https://files.pythonhosted.org/packages/81/0d/af9adb7a0e4420dcf249653f589cd27152fa6daab5cfd84e6d665dcd7df5/syrupy-4.7.1-py3-none-any.whl", hash = "sha256:be002267a512a4bedddfae2e026c93df1ea928ae10baadc09640516923376d41", size = 49135 }, 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "tornado" 1029 | version = "6.4.1" 1030 | source = { registry = "https://pypi.org/simple" } 1031 | sdist = { url = "https://files.pythonhosted.org/packages/ee/66/398ac7167f1c7835406888a386f6d0d26ee5dbf197d8a571300be57662d3/tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9", size = 500623 } 1032 | wheels = [ 1033 | { url = "https://files.pythonhosted.org/packages/00/d9/c33be3c1a7564f7d42d87a8d186371a75fd142097076767a5c27da941fef/tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8", size = 435924 }, 1034 | { url = "https://files.pythonhosted.org/packages/2e/0f/721e113a2fac2f1d7d124b3279a1da4c77622e104084f56119875019ffab/tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14", size = 433883 }, 1035 | { url = "https://files.pythonhosted.org/packages/13/cf/786b8f1e6fe1c7c675e79657448178ad65e41c1c9765ef82e7f6f765c4c5/tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4", size = 437224 }, 1036 | { url = "https://files.pythonhosted.org/packages/e4/8e/a6ce4b8d5935558828b0f30f3afcb2d980566718837b3365d98e34f6067e/tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842", size = 436597 }, 1037 | { url = "https://files.pythonhosted.org/packages/22/d4/54f9d12668b58336bd30defe0307e6c61589a3e687b05c366f804b7faaf0/tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3", size = 436797 }, 1038 | { url = "https://files.pythonhosted.org/packages/cf/3f/2c792e7afa7dd8b24fad7a2ed3c2f24a5ec5110c7b43a64cb6095cc106b8/tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f", size = 437516 }, 1039 | { url = "https://files.pythonhosted.org/packages/71/63/c8fc62745e669ac9009044b889fc531b6f88ac0f5f183cac79eaa950bb23/tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4", size = 436958 }, 1040 | { url = "https://files.pythonhosted.org/packages/94/d4/f8ac1f5bd22c15fad3b527e025ce219bd526acdbd903f52053df2baecc8b/tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698", size = 436882 }, 1041 | { url = "https://files.pythonhosted.org/packages/4b/3e/a8124c21cc0bbf144d7903d2a0cadab15cadaf683fa39a0f92bc567f0d4d/tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d", size = 438092 }, 1042 | { url = "https://files.pythonhosted.org/packages/d9/2f/3f2f05e84a7aff787a96d5fb06821323feb370fe0baed4db6ea7b1088f32/tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7", size = 438532 }, 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "tqdm" 1047 | version = "4.66.5" 1048 | source = { registry = "https://pypi.org/simple" } 1049 | dependencies = [ 1050 | { name = "colorama", marker = "platform_system == 'Windows'" }, 1051 | ] 1052 | sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 } 1053 | wheels = [ 1054 | { url = "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", size = 78351 }, 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "traitlets" 1059 | version = "5.14.3" 1060 | source = { registry = "https://pypi.org/simple" } 1061 | sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } 1062 | wheels = [ 1063 | { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "typing-extensions" 1068 | version = "4.12.2" 1069 | source = { registry = "https://pypi.org/simple" } 1070 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 1071 | wheels = [ 1072 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "urllib3" 1077 | version = "2.2.2" 1078 | source = { registry = "https://pypi.org/simple" } 1079 | sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266 } 1080 | wheels = [ 1081 | { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444 }, 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "wcwidth" 1086 | version = "0.2.13" 1087 | source = { registry = "https://pypi.org/simple" } 1088 | sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } 1089 | wheels = [ 1090 | { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, 1091 | ] 1092 | --------------------------------------------------------------------------------