├── .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 |

2 |
3 |
4 |
5 |
6 | 
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 |
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 | 
12 |
13 |
14 |
15 | You can then write a reply in handwriting:
16 |
17 | 
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 | 
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 |
--------------------------------------------------------------------------------