├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── Memo.png └── workflows │ └── ci.yml ├── .python-version ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── docs ├── .nav.yml ├── Getting started.md ├── Installation.md ├── index.md └── memo.png ├── mkdocs.yml ├── pyproject.toml ├── src ├── memo │ ├── __init__.py │ └── memo.py └── memo_helpers │ ├── __init__.py │ ├── add_memo.py │ ├── choice_memo.py │ ├── delete_memo.py │ ├── edit_memo.py │ ├── export_memo.py │ ├── get_memo.py │ ├── id_search_memo.py │ ├── list_folder.py │ ├── md_converter.py │ ├── move_memo.py │ ├── search_memo.py │ └── validation_memo.py ├── test ├── memo_notes_test.py └── memo_rem_test.py └── uv.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/Memo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoniorodr/memo/dba668d3620f22f71f463333ceacc0ec748d2575/.github/Memo.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: mkdocs-material 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | permissions: 8 | contents: write 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Configure Git Credentials 15 | run: | 16 | git config user.name github-actions[bot] 17 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: 3.x 21 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 22 | - uses: actions/cache@v4 23 | with: 24 | key: mkdocs-material-${{ env.cache_id }} 25 | path: .cache 26 | restore-keys: | 27 | mkdocs-material- 28 | - run: pip install ".[docs]" 29 | - run: mkdocs gh-deploy --force 30 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.3.1] - 05.05.2025 6 | 7 | ### Added 8 | 9 | - Material for MkDocs support for the documentation. 10 | 11 | ### Changed 12 | 13 | - Small changes and updates to the README file. 14 | 15 | ## [0.3.0] - 23.04.2025 16 | 17 | ### Added 18 | 19 | - Added support for exporting notes to the desktop and converting them to markdown format. Attachments and images will not be converted. 20 | 21 | ### Changed 22 | 23 | - Small changes and updates to the README file. 24 | 25 | ## [0.2.3] - 15.04.2025 26 | 27 | ### Changed 28 | 29 | - Added translations for the folder "Recently Deleted" to be able to filter it on all the languages MacOS supports. 30 | - Small changes and updates to the README file. 31 | 32 | ### Fixed 33 | 34 | - Fixed a bug caused by images on Notes when using the `-edit`flag. The images will not appear in the markdown preview (Temporary fix). 35 | 36 | ## [0.2.2] - 14.04.2025 37 | 38 | ### Added 39 | 40 | - Added support for editing Apple Reminders title and due date. 41 | 42 | ### Changed 43 | 44 | - Small changes and updates to the README file. 45 | - Refactored codebase and applied minor output improvements. 46 | 47 | ## [0.2.1] - 11.04.2025 48 | 49 | ### Added 50 | 51 | - Added support for delete Apple Notes folders with the `--remove` flag. 52 | 53 | ### Changed 54 | 55 | - Small changes and updates to the README file. 56 | 57 | ## [0.2.0] - 09.04.2025 58 | 59 | ### Added 60 | 61 | - Added functionality to `--delete` for Apple Notes. 62 | - Added support to Apple Reminders. Now you can create, delete and mark reminders as completed. 63 | - Basic test coverage for Apple Reminders. 64 | 65 | ### Changed 66 | 67 | - Improved the output of some of the flags, with colors and better formatting. 68 | - Small changes and updates to the README file. 69 | - Refactored codebase and applied minor output improvements. 70 | 71 | ## [0.1.2] - 08.04.2025 72 | 73 | ### Added 74 | 75 | - Added the `--search` flag to enable fuzzy searching of your notes. 76 | 77 | ### Changed 78 | 79 | - Refactored codebase and applied minor output improvements. 80 | 81 | ## [0.1.1] - 07.04.2025 82 | 83 | ### Added 84 | 85 | - Confirmation prompt when editing or moving notes that contain images or attachments. 86 | - Memo will notify you if the folder you are trying to filter does not exist when using `memo notes --folder `. 87 | - Basic test coverage. 88 | 89 | ### Changed 90 | 91 | - Refactored codebase and applied minor output improvements. 92 | 93 | ## [0.1.0] - 06.04.2025 94 | 95 | Initial Release. 96 | 97 | ### Added 98 | 99 | Initial release with core Apple Notes functionality: 100 | 101 | - Create new notes in Apple Notes 102 | - Edit existing notes 103 | - Delete notes 104 | - View a list of all notes 105 | - Move notes between folders 106 | - List all folders and subfolders 107 | 108 | [0.3.1]: https://github.com/antoniorodr/memo/releases/tag/v0.3.1 109 | [0.3.0]: htpps://github.com/antoniorodr/memo/releases/tag/v0.3.0 110 | [0.2.3]: https://github.com/antoniorodr/memo/releases/tag/v0.2.3 111 | [0.2.2]: https://github.com/antoniorodr/memo/releases/tag/v0.2.2 112 | [0.2.1]: https://github.com/antoniorodr/memo/releases/tag/v0.2.1 113 | [0.2.0]: https://github.com/antoniorodr/memo/releases/tag/v0.2.0 114 | [0.1.2]: https://github.com/antoniorodr/memo/releases/tag/v0.1.2 115 | [0.1.1]: https://github.com/antoniorodr/memo/releases/tag/v0.1.1 116 | [0.1.0]: https://github.com/antoniorodr/memo/releases/tag/v0.1.0 117 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | the following e-mail address 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | . Translations are available at 128 | . 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing! Whether it's a bug report, new feature, or fixing a typo — we appreciate it. 4 | 5 | All participation is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). 6 | 7 | ## Ways to Contribute 8 | 9 | - 🛠️ Make changes to code or docs (via PR) 10 | - 🐞 Report bugs 11 | - 💡 Suggest features 12 | - 💬 Join discussions 13 | 14 | ## Development Setup 15 | 16 | 1. Fork the project and clone your fork: 17 | 18 | ```bash 19 | git clone https://github.com/antoniorodr/memo 20 | cd memo 21 | ``` 22 | 23 | 2. Create a feature branch: 24 | 25 | ```bash 26 | git checkout -b my-feature 27 | ``` 28 | 29 | 3. Set up the environment with [uv](https://github.com/astral-sh/uv): 30 | 31 | ```bash 32 | uv venv 33 | source .venv/bin/activate 34 | uv sync 35 | ``` 36 | 37 | 4. (Optional) Uninstall Homebrew version of Memo: 38 | 39 | ```bash 40 | brew uninstall memo 41 | ``` 42 | 43 | 5. Install the CLI locally in editable mode: 44 | 45 | ```bash 46 | uv tool install . -e 47 | ``` 48 | 49 | 6. Run the tool: 50 | 51 | ```bash 52 | memo --help 53 | ``` 54 | 55 | 7. (Optional) Uninstall local version when you are done: 56 | 57 | ```bash 58 | uv tool uninstall memo 59 | ``` 60 | 61 | ## Commit Style 62 | 63 | Follow [Conventional Commits](https://www.conventionalcommits.org/) if possible: 64 | 65 | ``` 66 | feat: add export to JSON 67 | fix: handle missing config 68 | docs: improve usage section 69 | ``` 70 | 71 | ## Submitting Pull Requests 72 | 73 | 1. Push your feature branch: 74 | 75 | ```bash 76 | git push origin my-feature 77 | ``` 78 | 79 | 2. Open a pull request via GitHub’s web interface. 80 | 81 | Refer to [GitHub’s PR Guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) if you need help. 82 | 83 | ## Submitting Issues 84 | 85 | Use GitHub Issues to report bugs or suggest features. 86 | Use Discussions for open-ended ideas or proposals. 87 | 88 | ## License 89 | 90 | All contributions will be licensed under the same license as the project. 91 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Antonio Rodriguez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | memo 3 | 4 |   5 | 6 | 7 |
8 | 9 |

memo

10 | 11 |

12 | Github top language 13 | 14 | Github language count 15 | 16 | Repository size 17 | 18 | Github issues 19 | 20 | Github forks 21 | 22 | Github stars 23 | 24 |

25 | 26 |

27 | 🚧 memo 🚀 Under development... 🚧 28 |

29 | 30 |
31 | 32 |

33 | About   |   34 | Demo   |   35 | Features   |   36 | Technologies   |   37 | Installation   |   38 | Documentation   |   39 | Roadmap   |   40 | License 41 |

42 | 43 |
44 | 45 | ## :dart: About 46 | 47 | **Memo** is a simple command-line interface (CLI) tool for managing your Apple Notes and Apple Reminders. It’s written in Python and aims to offer a fast, keyboard-driven way to create, search, and organize notes and reminders straight from your terminal. 48 | 49 | ## :computer: Demo 50 | 51 | [![asciicast](https://asciinema.org/a/711983.svg)](https://asciinema.org/a/711983) 52 | 53 | ## :sparkles: Features 54 | 55 | :heavy_check_mark: View your notes and reminders directly from the terminal\ 56 | :heavy_check_mark: Edit your notes and reminders right from the terminal\ 57 | :heavy_check_mark: Add new notes and reminders effortlessly through the terminal\ 58 | :heavy_check_mark: Move notes to another folder effortlessly through the terminal\ 59 | :heavy_check_mark: Mark reminders as completed from the terminal\ 60 | :heavy_check_mark: Export your notes to HTML and convert them to Markdown 61 | 62 | ## :rocket: Technologies 63 | 64 | The following tools were used in this project: 65 | 66 | - [Click](https://click.palletsprojects.com/en/stable/) 67 | - [Mistune](https://mistune.lepture.com/en/latest/) 68 | - [html2text](https://pypi.org/project/html2text/) 69 | 70 | ## :checkered_flag: Installation 71 | 72 | #### Manual Installation 73 | 74 | ```bash 75 | git clone https://github.com/antoniorodr/memo 76 | 77 | cd memo 78 | 79 | pip install . 80 | ``` 81 | 82 | #### Homebrew Installation 83 | 84 | ```bash 85 | brew tap antoniorodr/memo 86 | brew install antoniorodr/memo/memo 87 | ``` 88 | 89 | ## :bookmark_tabs: Documentation 90 | 91 | :warning: Be careful when using --edit and --move flags with notes that include images/attachments. Memo does not support this yet. Memo will send you a warning if you try to edit a note with images/attachments. 92 | 93 | To read the full documentation, please visit the [docs](https://antoniorodr.github.io/memo) 94 | 95 | Use the command `memo notes --help` to see all the options available for notes. 96 | 97 | ```bash 98 | memo notes --help 99 | Usage: memo notes [OPTIONS] 100 | 101 | Options: 102 | -f, --folder TEXT Specify a folder to filter the notes (leave empty to get 103 | all). 104 | -a, --add Add a note to the specified folder. Specify a folder 105 | using the --folder flag. 106 | -e, --edit Edit a note in the specified folder. Specify a folder 107 | using the --folder flag. 108 | -d, --delete Delete a note in the specified folder. Specify a folder 109 | using the --folder flag. 110 | -m, --move Move a note to a different folder. 111 | -fl, --flist List all the folders and subfolders. 112 | -s, --search Fuzzy search your notes. 113 | -r, --remove Remove the folder you specified. 114 | -ex, --export Export your notes to the Desktop. 115 | --help Show this message and exit. 116 | ``` 117 | 118 | Use the command `memo rem --help` to see all the options available for reminders. 119 | 120 | ```bash 121 | memo rem --help 122 | Usage: memo rem [OPTIONS] 123 | 124 | Options: 125 | -c, --complete Mark a reminder as completed. 126 | -a, --add Add a new reminder. 127 | -d, --delete Delete a reminder. 128 | --help Show this message and exit. 129 | ``` 130 | 131 | You can use `memo --help` to see the available commands. 132 | 133 | ```bash 134 | memo --help 135 | Usage: memo [OPTIONS] COMMAND [ARGS]... 136 | 137 | Options: 138 | --version Show the version and exit. 139 | --help Show this message and exit. 140 | 141 | Commands: 142 | notes 143 | rem 144 | ``` 145 | 146 | Memo uses `$EDITOR` to edit and add notes. You can set it up by running the following command: 147 | 148 | ```bash 149 | export EDITOR="vim" 150 | ``` 151 | 152 | Where `vim` can be replaced with your preferred editor. Add it to your .zshrc/.bashrc to make it permanent. 153 | 154 | Or check the one you have set up in your terminal by running: 155 | 156 | ```bash 157 | echo $EDITOR 158 | ``` 159 | 160 | ## :pushpin: Roadmap 161 | 162 | - Check the roadmap [here](https://github.com/users/antoniorodr/projects/2) 163 | 164 | ## :memo: License 165 | 166 | This project is under license from MIT. For more details, see the [LICENSE](LICENSE.md) file. 167 | 168 | ## :eyes: Do you like my work? 169 | 170 | Buy Me A Coffee 171 | 172 | Made with :heart: by Antonio Rodriguez 173 | 174 |   175 | 176 | Back to top 177 | -------------------------------------------------------------------------------- /docs/.nav.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - index.md 3 | - Installation.md 4 | - Getting started.md 5 | -------------------------------------------------------------------------------- /docs/Getting started.md: -------------------------------------------------------------------------------- 1 | Be careful when using --edit and --move flags with notes that include images/attachments. Memo does not support this yet. Memo will send you a warning if you try to edit a note with images/attachments. 2 | 3 | Use the command `memo notes --help` to see all the options available for notes. 4 | 5 | ```bash 6 | memo notes --help 7 | Usage: memo notes [OPTIONS] 8 | 9 | Options: 10 | -f, --folder TEXT Specify a folder to filter the notes (leave empty to get 11 | all). 12 | -a, --add Add a note to the specified folder. Specify a folder 13 | using the --folder flag. 14 | -e, --edit Edit a note in the specified folder. Specify a folder 15 | using the --folder flag. 16 | -d, --delete Delete a note in the specified folder. Specify a folder 17 | using the --folder flag. 18 | -m, --move Move a note to a different folder. 19 | -fl, --flist List all the folders and subfolders. 20 | -s, --search Fuzzy search your notes. 21 | -r, --remove Remove the folder you specified. 22 | -ex, --export Export your notes to the Desktop. 23 | --help Show this message and exit. 24 | ``` 25 | 26 | Use the command `memo rem --help` to see all the options available for reminders. 27 | 28 | ```bash 29 | memo rem --help 30 | Usage: memo rem [OPTIONS] 31 | 32 | Options: 33 | -c, --complete Mark a reminder as completed. 34 | -a, --add Add a new reminder. 35 | -d, --delete Delete a reminder. 36 | --help Show this message and exit. 37 | ``` 38 | 39 | You can use `memo --help` to see the available commands. 40 | 41 | ```bash 42 | memo --help 43 | Usage: memo [OPTIONS] COMMAND [ARGS]... 44 | 45 | Options: 46 | --version Show the version and exit. 47 | --help Show this message and exit. 48 | 49 | Commands: 50 | notes 51 | rem 52 | ``` 53 | 54 | Memo uses `$EDITOR` to edit and add notes. You can set it up by running the following command: 55 | 56 | ```bash 57 | export EDITOR="vim" 58 | ``` 59 | 60 | Where `vim` can be replaced with your preferred editor. Add it to your .zshrc/.bashrc to make it permanent. 61 | 62 | Or check the one you have set up in your terminal by running: 63 | 64 | ```bash 65 | echo $EDITOR 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | #### Manual Installation 2 | 3 | ```bash 4 | git clone https://github.com/antoniorodr/memo 5 | 6 | cd memo 7 | 8 | pip install . 9 | ``` 10 | 11 | #### Homebrew Installation 12 | 13 | ```bash 14 | brew tap antoniorodr/memo 15 | brew install antoniorodr/memo/memo 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Memo's documentation 2 | 3 | ## About 4 | 5 | **Memo** is a simple command-line interface (CLI) tool for managing your Apple Notes and Apple Reminders. It’s written in Python and aims to offer a fast, keyboard-driven way to create, search, and organize notes and reminders straight from your terminal. 6 | 7 | ## Demo 8 | 9 | [![asciicast](https://asciinema.org/a/711983.svg)](https://asciinema.org/a/711983) 10 | -------------------------------------------------------------------------------- /docs/memo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoniorodr/memo/dba668d3620f22f71f463333ceacc0ec748d2575/docs/memo.png -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Memo 2 | site_url: https://mydomain.org/memo 3 | repo_url: https://github.com/antoniorodr/memo 4 | repo_name: Memo 5 | edit_uri: edit/main/docs/ 6 | theme: 7 | name: material 8 | logo: memo.png 9 | icon: 10 | repo: fontawesome/brands/github 11 | favicon: memo.png 12 | features: 13 | - navigation.instant 14 | - navigation.instant.prefetch 15 | - navigation.instant.progress 16 | - search.suggest 17 | - navigation.footer 18 | - navigation.path 19 | - navigation.top 20 | palette: 21 | - scheme: default 22 | primary: amber 23 | accent: amber 24 | toggle: 25 | icon: material/brightness-7 26 | name: Switch to dark mode 27 | 28 | - scheme: slate 29 | primary: amber 30 | accent: amber 31 | toggle: 32 | icon: material/brightness-4 33 | name: Switch to light mode 34 | primary: amber 35 | accent: amber 36 | plugins: 37 | - search 38 | - awesome-nav 39 | extra: 40 | social: 41 | - icon: fontawesome/brands/github 42 | link: https://github.com/antoniorodr 43 | copyright: Copyright © 2025 - Antonio Rodriguez 44 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "memo" 7 | authors = [{ name = "Antonio Rodriguez", email = "antonioinorge@hotmail.com" }] 8 | version = "0.3.1" 9 | description = "CLI app to manage your Apple Notes and Apple reminders" 10 | readme = "README.md" 11 | license = "MIT" 12 | requires-python = ">=3.13" 13 | dependencies = [ 14 | "chardet>=5.2.0", 15 | "click>=8.1.8", 16 | "html2text>=2024.2.26", 17 | "mistune>=3.1.3", 18 | "pytest>=8.3.5", 19 | ] 20 | 21 | 22 | [project.optional-dependencies] 23 | docs = ["mkdocs>=1.5.3", "mkdocs-material>=9.5.18", "mkdocs-awesome-nav>=3.1.1"] 24 | 25 | 26 | [project.urls] 27 | Repository = "https://github.com/antoniorodr/memo" 28 | Issues = "https://github.com/antoniorodr/memo/issues" 29 | 30 | [project.scripts] 31 | memo = "memo.memo:cli" 32 | 33 | [tool.setuptools] 34 | package-dir = { "" = "src" } 35 | 36 | [tool.pytest.ini_options] 37 | pythonpath = [".", "src"] 38 | -------------------------------------------------------------------------------- /src/memo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoniorodr/memo/dba668d3620f22f71f463333ceacc0ec748d2575/src/memo/__init__.py -------------------------------------------------------------------------------- /src/memo/memo.py: -------------------------------------------------------------------------------- 1 | import click 2 | import datetime 3 | from memo_helpers.get_memo import get_note, get_reminder 4 | from memo_helpers.edit_memo import edit_note, edit_reminder 5 | from memo_helpers.add_memo import add_note, add_reminder 6 | from memo_helpers.delete_memo import ( 7 | delete_note, 8 | complete_reminder, 9 | delete_reminder, 10 | delete_note_folder, 11 | ) 12 | from memo_helpers.move_memo import move_note 13 | from memo_helpers.choice_memo import pick_note, pick_reminder 14 | from memo_helpers.list_folder import notes_folders 15 | from memo_helpers.validation_memo import selection_notes_validation 16 | from memo_helpers.search_memo import fuzzy_notes 17 | from memo_helpers.export_memo import export_memo 18 | 19 | # TODO: Check if notes can be imported and exported. 20 | # TODO: Check if its possible to fetch .localized names from the folders. 21 | # TODO: Check alternative to md_converter to support images and attachments. 22 | 23 | 24 | @click.group(invoke_without_command=False) 25 | @click.version_option() 26 | def cli(): 27 | pass 28 | 29 | 30 | @cli.command() 31 | @click.option( 32 | "--folder", 33 | "-f", 34 | default="", 35 | help="Specify a folder to filter the notes (leave empty to get all).", 36 | ) 37 | @click.option( 38 | "--add", 39 | "-a", 40 | is_flag=True, 41 | help="Add a note to the specified folder. Specify a folder using the --folder flag.", 42 | ) 43 | @click.option( 44 | "--edit", 45 | "-e", 46 | is_flag=True, 47 | help="Edit a note in the specified folder. Specify a folder using the --folder flag.", 48 | ) 49 | @click.option( 50 | "--delete", 51 | "-d", 52 | is_flag=True, 53 | help="Delete a note in the specified folder. Specify a folder using the --folder flag.", 54 | ) 55 | @click.option( 56 | "--move", 57 | "-m", 58 | is_flag=True, 59 | help="Move a note to a different folder.", 60 | ) 61 | @click.option( 62 | "--flist", 63 | "-fl", 64 | is_flag=True, 65 | help="List all the folders and subfolders.", 66 | ) 67 | @click.option("--search", "-s", is_flag=True, help="Fuzzy search your notes.") 68 | @click.option( 69 | "--remove", 70 | "-r", 71 | is_flag=True, 72 | help="Remove the folder you specified.", 73 | ) 74 | @click.option( 75 | "--export", 76 | "-ex", 77 | is_flag=True, 78 | help="Export your notes to the Desktop.", 79 | ) 80 | def notes(folder, edit, add, delete, move, flist, search, remove, export): 81 | selection_notes_validation( 82 | folder, edit, delete, move, add, flist, search, remove, export 83 | ) 84 | notes_info = get_note() 85 | note_map = notes_info[0] 86 | notes_list = notes_info[1] 87 | notes_list_filter = [ 88 | note for note in enumerate(notes_list, start=1) if folder in note[1] 89 | ] 90 | folders = notes_folders() 91 | 92 | if not flist and not search and not remove and not export: 93 | click.secho("\nFetching notes...", fg="yellow") 94 | if folder not in folders: 95 | click.echo("\nThe folder does not exists.") 96 | click.echo("\nUse 'memo notes -fl' to see your folders") 97 | elif not notes_list_filter: 98 | click.echo("\nNo notes found.") 99 | else: 100 | title = f"Your Notes in folder {folder}:" if folder else "All your notes:" 101 | click.echo(f"\n{title}\n") 102 | for note in notes_list_filter: 103 | click.echo(f"{note[0]}. {note[1]}") 104 | 105 | if edit: 106 | note_id = pick_note(note_map, notes_list_filter, "edit") 107 | edit_note(note_id) 108 | if add: 109 | add_note(folder) 110 | if move: 111 | note_id = pick_note(note_map, notes_list_filter, "move") 112 | if note_id is None: 113 | click.echo("Invalid selection.") 114 | return 115 | target_folder = click.prompt( 116 | "\nEnter the folder you want to move the note to", type=str 117 | ) 118 | move_note(note_id, target_folder) 119 | if delete: 120 | note_id = pick_note(note_map, notes_list_filter, "delete") 121 | delete_note(note_id) 122 | if flist: 123 | click.echo("\nFolders and subfolders in Notes:") 124 | click.echo(f"\n{folders}") 125 | if search: 126 | click.secho("\nFetching notes...\n", fg="yellow") 127 | fuzzy_notes() 128 | if remove: 129 | click.echo(f"\n{folders}") 130 | click.secho( 131 | "\n⚠️ Make sure the folder is empty, because the notes it includes will be deleted too.", 132 | fg="red", 133 | ) 134 | folder_to_delete = click.prompt( 135 | "\nEnter the name of the folder to delete", 136 | type=str, 137 | ) 138 | delete_note_folder(folder_to_delete) 139 | if export: 140 | if click.confirm("\nAre you sure you want to export your notes to HTML?"): 141 | export_memo() 142 | 143 | 144 | @cli.command() 145 | @click.option( 146 | "--complete", 147 | "-c", 148 | is_flag=True, 149 | help="Mark a reminder as completed.", 150 | ) 151 | @click.option( 152 | "--add", 153 | "-a", 154 | is_flag=True, 155 | help="Add a new reminder.", 156 | ) 157 | @click.option( 158 | "--delete", 159 | "-d", 160 | is_flag=True, 161 | help="Delete a reminder.", 162 | ) 163 | @click.option( 164 | "--edit", 165 | "-e", 166 | is_flag=True, 167 | help="Edit a reminder.", 168 | ) 169 | def rem(complete, add, delete, edit): 170 | if add: 171 | add_reminder() 172 | else: 173 | today = datetime.datetime.today() 174 | modified_today = today - datetime.timedelta(days=1) 175 | reminders_info = get_reminder() 176 | reminders_map = reminders_info[0] 177 | reminders_list = reminders_info[1] 178 | reminders_list_filter = [ 179 | reminder for reminder in enumerate(reminders_list, start=1) 180 | ] 181 | click.echo("\nYour Reminders:\n") 182 | for reminder in reminders_list_filter: 183 | reminder_dato = datetime.datetime.strptime( 184 | reminder[1].split(" | ")[1], "%Y-%m-%d %H:%M:%S" 185 | ) 186 | dato_diff = reminder_dato - modified_today 187 | if dato_diff.days <= 1: 188 | due = ( 189 | f"Due on {dato_diff.days} day" 190 | if dato_diff.days > 0 191 | else "Due today" 192 | ) 193 | click.secho( 194 | f"{reminder[0]}. {reminder[1]} | {due}", 195 | fg="red", 196 | ) 197 | elif dato_diff.days <= 3: 198 | click.secho( 199 | f"{reminder[0]}. {reminder[1]} | Due on {dato_diff.days} days", 200 | fg="yellow", 201 | ) 202 | else: 203 | click.echo( 204 | f"{reminder[0]}. {reminder[1]} | Due on {dato_diff.days} days" 205 | ) 206 | if complete: 207 | reminder_id = pick_reminder( 208 | reminders_map, reminders_list_filter, "complete" 209 | ) 210 | complete_reminder(reminder_id) 211 | if delete: 212 | reminder_id = pick_reminder(reminders_map, reminders_list_filter, "delete") 213 | delete_reminder(reminder_id) 214 | if edit: 215 | reminder_id = pick_reminder(reminders_map, reminders_list_filter, "edit") 216 | part_to_edit = ( 217 | click.prompt("\nEnter the part to edit ('title' or 'due date')") 218 | .strip() 219 | .lower() 220 | ) 221 | edit_reminder(reminder_id, part_to_edit) 222 | -------------------------------------------------------------------------------- /src/memo_helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoniorodr/memo/dba668d3620f22f71f463333ceacc0ec748d2575/src/memo_helpers/__init__.py -------------------------------------------------------------------------------- /src/memo_helpers/add_memo.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import click 3 | import tempfile 4 | import mistune 5 | import os 6 | from datetime import datetime 7 | 8 | 9 | def add_note(folder_name): 10 | with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as temp_file: 11 | temp_file.write(b"# New Note\n\nWrite your note here...") 12 | temp_file_path = temp_file.name 13 | 14 | editor = os.getenv("EDITOR", "vim") 15 | subprocess.run([editor, temp_file_path]) 16 | 17 | with open(temp_file_path, "r", encoding="utf-8") as file: 18 | note_md = file.read().strip() 19 | 20 | if not note_md or note_md == "# New Note\n\nWrite your note here...": 21 | click.echo("\nNote creation cancelled.") 22 | os.remove(temp_file_path) 23 | return 24 | 25 | note_html = mistune.markdown(note_md) 26 | 27 | script = f""" 28 | tell application "Notes" 29 | set targetFolder to first folder whose name is "{folder_name}" 30 | tell targetFolder 31 | make new note with properties {{name:"New Note", body:"{note_html}"}} 32 | end tell 33 | end tell 34 | """ 35 | 36 | process = subprocess.run( 37 | ["osascript", "-e", script], capture_output=True, text=True 38 | ) 39 | 40 | os.remove(temp_file_path) 41 | 42 | if process.returncode == 0: 43 | click.echo(f"\nNote created in '{folder_name}' folder.") 44 | else: 45 | click.echo("\nError: Could not create note. Check if the folder exists.") 46 | 47 | 48 | def add_reminder(): 49 | title = click.prompt("\nEnter the title of the reminder") 50 | date = click.prompt("Enter the due date (YYYY-MM-DD)") 51 | time = click.prompt("Enter the due time (HH:MM)") 52 | datetime_str = f"{date} {time}" 53 | due_dt = datetime.strptime(datetime_str, "%Y-%m-%d %H:%M") 54 | 55 | year = due_dt.year 56 | month = due_dt.month 57 | day = due_dt.day 58 | hour = due_dt.hour 59 | minute = due_dt.minute 60 | 61 | script = f''' 62 | tell application "Reminders" 63 | set theDate to current date 64 | set year of theDate to {year} 65 | set month of theDate to {month} 66 | set day of theDate to {day} 67 | set time of theDate to ({hour} * hours + {minute} * minutes) 68 | make new reminder with properties {{name:"{title}", due date:theDate}} 69 | end tell 70 | ''' 71 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 72 | 73 | if result.returncode == 0: 74 | click.secho(f"\nReminder '{title}' added successfully.", fg="green") 75 | else: 76 | click.secho(f"\nError: Could not add reminder, {result.stderr}", fg="red") 77 | -------------------------------------------------------------------------------- /src/memo_helpers/choice_memo.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | def pick_note(note_map, notes_list, action): 5 | choice = click.prompt( 6 | f"\nEnter the number of the note you want to {action}", type=int 7 | ) 8 | if 1 <= choice <= len(notes_list): 9 | note_data = note_map.get(choice) 10 | if note_data is None: 11 | click.echo("Invalid selection.") 12 | return 13 | return note_data[0] 14 | else: 15 | raise IndexError("The note you selected is not in the list.") 16 | 17 | 18 | def pick_reminder(reminder_map, reminders_list, action): 19 | choice = click.prompt( 20 | f"\nEnter the number of the reminder you want to {action}", type=int 21 | ) 22 | if 1 <= choice <= len(reminders_list): 23 | reminder_data = reminder_map.get(choice) 24 | if reminder_data is None: 25 | click.echo("Invalid selection.") 26 | return 27 | return reminder_data[0] 28 | else: 29 | raise IndexError("The reminder you selected is not in the list.") 30 | -------------------------------------------------------------------------------- /src/memo_helpers/delete_memo.py: -------------------------------------------------------------------------------- 1 | import click 2 | import subprocess 3 | 4 | 5 | def delete_note(note_id): 6 | script = f''' 7 | tell application "Notes" 8 | set theNote to first note whose id is "{note_id}" 9 | delete theNote 10 | end tell 11 | ''' 12 | 13 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 14 | 15 | if result.returncode == 0: 16 | click.secho("\nNote deleted successfully.", fg="green") 17 | else: 18 | click.secho(f"Error: {result.stderr}", fg="red") 19 | 20 | 21 | def delete_note_folder(folder_name): 22 | script = f''' 23 | tell application "Notes" 24 | set selectedFolder to first folder whose name is "{folder_name}" 25 | delete selectedFolder 26 | end tell 27 | ''' 28 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 29 | 30 | if result.returncode == 0: 31 | click.secho("\nFolder deleted successfully.", fg="green") 32 | else: 33 | click.secho(f"Error: {result.stderr}", fg="red") 34 | 35 | 36 | def complete_reminder(reminder_id): 37 | script = f''' 38 | tell application "Reminders" 39 | set selectedRem to first reminder whose id is "{reminder_id}" 40 | set completed of selectedRem to true 41 | end tell 42 | ''' 43 | 44 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 45 | 46 | if result.returncode == 0: 47 | click.secho("\nReminder marked successfully as completed.", fg="green") 48 | else: 49 | click.secho(f"Error: {result.stderr}", fg="red") 50 | 51 | 52 | def delete_reminder(reminder_id): 53 | script = f''' 54 | tell application "Reminders" 55 | set selectedRem to first reminder whose id is "{reminder_id}" 56 | delete selectedRem 57 | end tell 58 | ''' 59 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 60 | 61 | if result.returncode == 0: 62 | click.secho("\nReminder deleted successfully.", fg="green") 63 | else: 64 | click.secho(f"Error: {result.stderr}", fg="red") 65 | -------------------------------------------------------------------------------- /src/memo_helpers/edit_memo.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import click 3 | import tempfile 4 | import mistune 5 | import os 6 | import datetime 7 | from memo_helpers.id_search_memo import id_search_memo 8 | from memo_helpers.md_converter import md_converter 9 | 10 | 11 | def edit_note(note_id): 12 | result = id_search_memo(note_id) 13 | original_md, original_html = md_converter(result) 14 | 15 | with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as temp_file: 16 | temp_file.write(original_md.encode("utf-8")) 17 | temp_file_path = temp_file.name 18 | 19 | if " 250 then 29 | set t to text 1 thru 250 of t 30 | end if 31 | return t 32 | end cleanFileName 33 | 34 | tell application "Notes" 35 | repeat with theNote in notes of default account 36 | set noteLocked to password protected of theNote as boolean 37 | if not noteLocked then 38 | set noteName to name of theNote as string 39 | set noteBody to body of theNote as string 40 | set cleanName to my cleanFileName(noteName) 41 | set exportPath to exportFolder & cleanName 42 | set tempHTMLPath to exportPath & ".html" 43 | set htmlContent to "" & noteBody & "" 44 | set f to open for access (POSIX file tempHTMLPath) with write permission 45 | set eof of f to 0 46 | write htmlContent to f 47 | close access f 48 | end if 49 | end repeat 50 | end tell 51 | """ 52 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 53 | if result.returncode == 0: 54 | click.secho("\nNotes exported to Desktop", fg="green") 55 | if click.confirm( 56 | "\nDo you want to convert the notes to Markdown? Attachements and pictures will not be converted." 57 | ): 58 | html_to_md() 59 | else: 60 | click.secho("\nError exporting notes", fg="red") 61 | 62 | 63 | def html_to_md(): 64 | files = os.listdir(EXPORT_PATH) 65 | files_list = [f for f in files if os.path.isfile(os.path.join(EXPORT_PATH, f))] 66 | 67 | for file in files_list: 68 | file_path = os.path.join(EXPORT_PATH, file) 69 | file_name = os.path.splitext(file)[0] 70 | 71 | with open(file_path, "rb") as f: 72 | raw_data = f.read() 73 | result = chardet.detect(raw_data) 74 | encoding = result["encoding"] 75 | 76 | try: 77 | if encoding: 78 | html_content = raw_data.decode(encoding) 79 | else: 80 | html_content = raw_data.decode("utf-8", errors="replace") 81 | except Exception as e: 82 | click.secho( 83 | f"Could not decode {file} with detected encoding '{encoding}': {e}", 84 | fg="red", 85 | ) 86 | return 87 | 88 | text_maker = html2text.HTML2Text() 89 | text_maker.images_to_alt = True 90 | text_maker.body_width = 0 91 | original_md = text_maker.handle(html_content).strip() 92 | output_path = os.path.join(EXPORT_PATH, f"{file_name}.md") 93 | 94 | with open(output_path, "w", encoding="utf-8") as md_file: 95 | md_file.write(original_md) 96 | 97 | click.secho("\nAll notes succesfully converted to Markdown", fg="green") 98 | -------------------------------------------------------------------------------- /src/memo_helpers/get_memo.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import click 3 | import datetime 4 | 5 | 6 | def get_note(): 7 | script = """ 8 | set deletedTranslations to {"Recently Deleted", "Nylig slettet", "Senast raderade", "Senest slettet", "Zuletzt gelöscht", "Supprimés récemment", "Eliminados recientemente", "Eliminati di recente", "Recent verwijderd", "Ostatnio usunięte", "Недавно удалённые", "Apagados recentemente", "Apagadas recentemente", "最近删除", "最近刪除", "最近削除した項目", "최근 삭제된 항목", "Son Silinenler", "Äskettäin poistetut", "Nedávno smazané", "Πρόσφατα διαγραμμένα", "Nemrég töröltek", "Șterse recent", "Nedávno vymazané", "เพิ่งลบ", "Đã xóa gần đây", "Нещодавно видалені"} 9 | 10 | tell application "Notes" 11 | set output to "" 12 | repeat with eachFolder in folders 13 | set folderName to name of eachFolder 14 | if folderName is not in deletedTranslations then 15 | repeat with eachNote in notes of eachFolder 16 | set noteName to name of eachNote 17 | set noteID to id of eachNote 18 | set output to output & noteID & "|" & folderName & " - " & noteName & "\n" 19 | end repeat 20 | end if 21 | end repeat 22 | return output 23 | end tell 24 | """ 25 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 26 | notes_list = [line.split("|") for line in result.stdout.strip().split("\n") if line] 27 | note_map = { 28 | i + 1: (note_id, note_title) 29 | for i, (note_id, note_title) in enumerate(notes_list) 30 | } 31 | 32 | if not notes_list: 33 | click.echo("No notes found.") 34 | seen_id = set() 35 | notes_list = [ 36 | note_title 37 | for _, (id, note_title) in note_map.items() 38 | if id not in seen_id and not seen_id.add(id) 39 | ] 40 | return [note_map, notes_list] 41 | 42 | 43 | def get_reminder(): 44 | click.secho("\nFetching reminders...", fg="yellow") 45 | script = """ 46 | set output to "" 47 | tell application "Reminders" 48 | repeat with eachRem in reminders 49 | if not completed of eachRem then 50 | set nameRem to name of eachRem 51 | set idRem to id of eachRem 52 | set dueDateRem to due date of eachRem 53 | if dueDateRem is not missing value then 54 | set timeStamp to (dueDateRem - (current date)) + (do shell script "date +%s") as real 55 | else 56 | set timeStamp to "None" 57 | end if 58 | set output to output & idRem & "|" & nameRem & " -> " & timeStamp & "\\n" 59 | end if 60 | end repeat 61 | end tell 62 | return output 63 | """ 64 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 65 | reminders_list = [ 66 | line.split("|") for line in result.stdout.strip().split("\n") if line 67 | ] 68 | reminders_map = {} 69 | for i, (reminder_id, reminder_title) in enumerate(reminders_list): 70 | parts = reminder_title.split("->") 71 | title = parts[0].strip() 72 | due_ts_raw = parts[1].strip() 73 | 74 | if due_ts_raw != "None": 75 | due_ts_clean = due_ts_raw.replace(",", ".") 76 | try: 77 | due_datetime = datetime.datetime.fromtimestamp(float(due_ts_clean)) 78 | except ValueError: 79 | due_datetime = None 80 | else: 81 | due_datetime = datetime.datetime.today().strftime("%Y-%m-%d %H:%M:%S") 82 | 83 | reminders_map[i + 1] = (reminder_id, title, due_datetime) 84 | 85 | reminders_list = [f"{v[1]} | {v[2]}" for v in reminders_map.values()] 86 | return [reminders_map, reminders_list] 87 | -------------------------------------------------------------------------------- /src/memo_helpers/id_search_memo.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | 4 | def id_search_memo(note_id): 5 | script = f""" 6 | tell application "Notes" 7 | set selectedNote to first note whose id is "{note_id}" 8 | return body of selectedNote 9 | end tell 10 | """ 11 | result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) 12 | return result 13 | -------------------------------------------------------------------------------- /src/memo_helpers/list_folder.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import click 3 | 4 | 5 | def notes_folders(): 6 | script = """ 7 | tell application "Notes" 8 | set topLevelFolders to every folder 9 | set folderHierarchy to my listFolders(topLevelFolders, 0) 10 | return folderHierarchy 11 | end tell 12 | on listFolders(folders, indentLevel) 13 | set hierarchyText to "" 14 | set rootFolders to folders 15 | repeat with f in rootFolders 16 | set folderName to name of f 17 | set indent to my repeatChar(" ", indentLevel) 18 | set hierarchyText to hierarchyText & indent & folderName & return 19 | set subFolders to folder of f 20 | set xcount to count subFolders 21 | if xcount > 0 then 22 | set hierarchyText to hierarchyText & my listFolders(subFolders, indentLevel + 2) 23 | end if 24 | end repeat 25 | return hierarchyText 26 | end listFolders 27 | on repeatChar(theChar, xcount) 28 | set theString to "" 29 | repeat xcount times 30 | set theString to theString & theChar 31 | end repeat 32 | return theString 33 | end repeatChar 34 | """ 35 | 36 | try: 37 | result = subprocess.run( 38 | ["osascript", "-e", script], capture_output=True, text=True, check=True 39 | ) 40 | return result.stdout.strip() 41 | except subprocess.CalledProcessError as e: 42 | click.echo(f"Error running AppleScript: {e}") 43 | -------------------------------------------------------------------------------- /src/memo_helpers/md_converter.py: -------------------------------------------------------------------------------- 1 | import html2text 2 | 3 | 4 | def md_converter(id_search_result): 5 | original_html = id_search_result.stdout.strip() 6 | 7 | text_maker = html2text.HTML2Text() 8 | text_maker.images_to_alt = True 9 | text_maker.body_width = 0 10 | original_md = text_maker.handle(original_html).strip() 11 | return [original_md, original_html] 12 | -------------------------------------------------------------------------------- /src/memo_helpers/move_memo.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import click 3 | import html2text 4 | from memo_helpers.id_search_memo import id_search_memo 5 | 6 | 7 | def move_note(note_id: str, target_folder: str): 8 | result = id_search_memo(note_id) 9 | original_html = result.stdout.strip() 10 | 11 | text_maker = html2text.HTML2Text() 12 | text_maker.body_width = 0 13 | 14 | if " 1: 25 | raise click.UsageError( 26 | "--flist must be used alone. It cannot be combined with other flags or --folder." 27 | ) 28 | 29 | modifier_flags = ["edit", "delete", "move", "remove", "search", "export"] 30 | used_modifiers = [f for f in modifier_flags if used_flags[f]] 31 | if len(used_modifiers) > 1: 32 | raise click.UsageError( 33 | "Only one of --edit, --delete, --move, --remove , --export or search can be used at a time." 34 | ) 35 | -------------------------------------------------------------------------------- /test/memo_notes_test.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | from memo.memo import cli 3 | 4 | 5 | def test_notes(): 6 | runner = CliRunner() 7 | result = runner.invoke(cli, ["notes"]) 8 | assert result.exit_code == 0 9 | assert "All your notes:" in result.output 10 | 11 | 12 | def test_notes_folder_without_folder_name(): 13 | runner = CliRunner() 14 | result = runner.invoke(cli, ["notes", "--folder"]) 15 | assert result.exit_code == 2 16 | assert "Error: Option '--folder' requires an argument." in result.output 17 | 18 | 19 | def test_notes_folder_not_exists(): 20 | runner = CliRunner() 21 | result = runner.invoke(cli, ["notes", "--folder", "ksndclskdnc"]) 22 | assert result.exit_code == 0 23 | assert "The folder does not exists." in result.output 24 | 25 | 26 | def test_notes_add_no_folder(): 27 | runner = CliRunner() 28 | result = runner.invoke(cli, ["notes", "--add"]) 29 | assert result.exit_code == 2 30 | assert ( 31 | "Error: --add must be used indicating a folder to create the note to." 32 | in result.output 33 | ) 34 | 35 | 36 | def test_notes_edit(): 37 | runner = CliRunner() 38 | result = runner.invoke(cli, ["notes", "--edit"], input="1") 39 | assert result.exit_code == 0 40 | assert "Enter the number of the note you want to edit:" in result.output 41 | 42 | 43 | def test_notes_edit_indexerror(): 44 | runner = CliRunner() 45 | result = runner.invoke(cli, ["notes", "--edit"], input="9999") 46 | assert result.exit_code == 1 47 | 48 | 49 | def test_notes_delete(): 50 | runner = CliRunner() 51 | result = runner.invoke(cli, ["notes", "--delete"], input="1") 52 | assert result.exit_code == 0 53 | assert "Note deleted successfully." in result.output 54 | 55 | 56 | def test_notes_delete_indexerror(): 57 | runner = CliRunner() 58 | result = runner.invoke(cli, ["notes", "--delete"], input="9999") 59 | assert result.exit_code == 1 60 | 61 | 62 | def test_notes_move_indexerror(): 63 | runner = CliRunner() 64 | result = runner.invoke(cli, ["notes", "--move"], input="9999") 65 | assert result.exit_code == 1 66 | 67 | 68 | def test_notes_flist(): 69 | runner = CliRunner() 70 | result = runner.invoke(cli, ["notes", "--flist"]) 71 | assert result.exit_code == 0 72 | assert "Folders and subfolders in Notes:" in result.output 73 | -------------------------------------------------------------------------------- /test/memo_rem_test.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | from memo.memo import cli 3 | 4 | 5 | def test_rem(): 6 | runner = CliRunner() 7 | result = runner.invoke(cli, ["rem"]) 8 | assert result.exit_code == 0 9 | assert "Your Reminders:" in result.output 10 | 11 | 12 | def test_rem_complete(): 13 | runner = CliRunner() 14 | result = runner.invoke(cli, ["rem", "--complete"], input="1") 15 | assert result.exit_code == 0 16 | assert "Reminder marked successfully as completed." in result.output 17 | 18 | 19 | def test_rem_delete(): 20 | runner = CliRunner() 21 | result = runner.invoke(cli, ["rem", "--delete"], input="1") 22 | assert result.exit_code == 0 23 | assert "Reminder deleted successfully." in result.output 24 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "bracex" 16 | version = "2.5.post1" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641, upload-time = "2024-09-28T21:41:22.017Z" } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558, upload-time = "2024-09-28T21:41:21.016Z" }, 21 | ] 22 | 23 | [[package]] 24 | name = "chardet" 25 | version = "5.2.0" 26 | source = { registry = "https://pypi.org/simple" } 27 | sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } 28 | wheels = [ 29 | { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, 30 | ] 31 | 32 | [[package]] 33 | name = "click" 34 | version = "8.1.8" 35 | source = { registry = "https://pypi.org/simple" } 36 | dependencies = [ 37 | { name = "colorama", marker = "sys_platform == 'win32'" }, 38 | ] 39 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, 42 | ] 43 | 44 | [[package]] 45 | name = "colorama" 46 | version = "0.4.6" 47 | source = { registry = "https://pypi.org/simple" } 48 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 49 | wheels = [ 50 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 51 | ] 52 | 53 | [[package]] 54 | name = "ghp-import" 55 | version = "2.1.0" 56 | source = { registry = "https://pypi.org/simple" } 57 | dependencies = [ 58 | { name = "python-dateutil" }, 59 | ] 60 | sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } 61 | wheels = [ 62 | { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, 63 | ] 64 | 65 | [[package]] 66 | name = "html2text" 67 | version = "2024.2.26" 68 | source = { registry = "https://pypi.org/simple" } 69 | sdist = { url = "https://files.pythonhosted.org/packages/1a/43/e1d53588561e533212117750ee79ad0ba02a41f52a08c1df3396bd466c05/html2text-2024.2.26.tar.gz", hash = "sha256:05f8e367d15aaabc96415376776cdd11afd5127a77fce6e36afc60c563ca2c32", size = 56527, upload-time = "2024-02-27T18:49:24.855Z" } 70 | 71 | [[package]] 72 | name = "iniconfig" 73 | version = "2.1.0" 74 | source = { registry = "https://pypi.org/simple" } 75 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 76 | wheels = [ 77 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 78 | ] 79 | 80 | [[package]] 81 | name = "jinja2" 82 | version = "3.1.6" 83 | source = { registry = "https://pypi.org/simple" } 84 | dependencies = [ 85 | { name = "markupsafe" }, 86 | ] 87 | sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } 88 | wheels = [ 89 | { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, 90 | ] 91 | 92 | [[package]] 93 | name = "markdown" 94 | version = "3.8" 95 | source = { registry = "https://pypi.org/simple" } 96 | sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload-time = "2025-04-11T14:42:50.928Z" } 97 | wheels = [ 98 | { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload-time = "2025-04-11T14:42:49.178Z" }, 99 | ] 100 | 101 | [[package]] 102 | name = "markupsafe" 103 | version = "3.0.2" 104 | source = { registry = "https://pypi.org/simple" } 105 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } 106 | wheels = [ 107 | { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, 108 | { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, 109 | { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, 110 | { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, 111 | { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, 112 | { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, 113 | { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, 114 | { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, 115 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, 116 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, 117 | { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, 118 | { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, 119 | { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, 120 | { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, 121 | { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, 122 | { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, 123 | { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, 124 | { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, 125 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, 126 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, 127 | ] 128 | 129 | [[package]] 130 | name = "memo" 131 | version = "0.3.1" 132 | source = { editable = "." } 133 | dependencies = [ 134 | { name = "chardet" }, 135 | { name = "click" }, 136 | { name = "html2text" }, 137 | { name = "mistune" }, 138 | { name = "mkdocs-awesome-nav" }, 139 | { name = "pytest" }, 140 | ] 141 | 142 | [package.metadata] 143 | requires-dist = [ 144 | { name = "chardet", specifier = ">=5.2.0" }, 145 | { name = "click", specifier = ">=8.1.8" }, 146 | { name = "html2text", specifier = ">=2024.2.26" }, 147 | { name = "mistune", specifier = ">=3.1.3" }, 148 | { name = "mkdocs-awesome-nav", specifier = ">=3.1.1" }, 149 | { name = "pytest", specifier = ">=8.3.5" }, 150 | ] 151 | 152 | [[package]] 153 | name = "mergedeep" 154 | version = "1.3.4" 155 | source = { registry = "https://pypi.org/simple" } 156 | sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } 157 | wheels = [ 158 | { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, 159 | ] 160 | 161 | [[package]] 162 | name = "mistune" 163 | version = "3.1.3" 164 | source = { registry = "https://pypi.org/simple" } 165 | sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347, upload-time = "2025-03-19T14:27:24.955Z" } 166 | wheels = [ 167 | { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload-time = "2025-03-19T14:27:23.451Z" }, 168 | ] 169 | 170 | [[package]] 171 | name = "mkdocs" 172 | version = "1.6.1" 173 | source = { registry = "https://pypi.org/simple" } 174 | dependencies = [ 175 | { name = "click" }, 176 | { name = "colorama", marker = "sys_platform == 'win32'" }, 177 | { name = "ghp-import" }, 178 | { name = "jinja2" }, 179 | { name = "markdown" }, 180 | { name = "markupsafe" }, 181 | { name = "mergedeep" }, 182 | { name = "mkdocs-get-deps" }, 183 | { name = "packaging" }, 184 | { name = "pathspec" }, 185 | { name = "pyyaml" }, 186 | { name = "pyyaml-env-tag" }, 187 | { name = "watchdog" }, 188 | ] 189 | sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } 190 | wheels = [ 191 | { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, 192 | ] 193 | 194 | [[package]] 195 | name = "mkdocs-awesome-nav" 196 | version = "3.1.1" 197 | source = { registry = "https://pypi.org/simple" } 198 | dependencies = [ 199 | { name = "mkdocs" }, 200 | { name = "natsort" }, 201 | { name = "pydantic" }, 202 | { name = "wcmatch" }, 203 | ] 204 | sdist = { url = "https://files.pythonhosted.org/packages/2d/e0/b2185370a35ecc042f648beec27a6d1a5d15d2ad46255d68201d2ba9cc38/mkdocs_awesome_nav-3.1.1.tar.gz", hash = "sha256:0665ab290a3c22b49e07151e3aca82d5d28a42aecf6e940bf295546565d156b5", size = 8656, upload-time = "2025-04-06T01:46:59.045Z" } 205 | wheels = [ 206 | { url = "https://files.pythonhosted.org/packages/0b/03/84bdfc698c5307259b269c5c20b8d62c7f2f36667f0a10d4d311dc604546/mkdocs_awesome_nav-3.1.1-py3-none-any.whl", hash = "sha256:50eafd8042274feff2b995ecaa50c55c07807ff33210a064877f6f1aa8d6ee40", size = 12356, upload-time = "2025-04-06T01:46:57.891Z" }, 207 | ] 208 | 209 | [[package]] 210 | name = "mkdocs-get-deps" 211 | version = "0.2.0" 212 | source = { registry = "https://pypi.org/simple" } 213 | dependencies = [ 214 | { name = "mergedeep" }, 215 | { name = "platformdirs" }, 216 | { name = "pyyaml" }, 217 | ] 218 | sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } 219 | wheels = [ 220 | { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, 221 | ] 222 | 223 | [[package]] 224 | name = "natsort" 225 | version = "8.4.0" 226 | source = { registry = "https://pypi.org/simple" } 227 | sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } 228 | wheels = [ 229 | { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, 230 | ] 231 | 232 | [[package]] 233 | name = "packaging" 234 | version = "24.2" 235 | source = { registry = "https://pypi.org/simple" } 236 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } 237 | wheels = [ 238 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, 239 | ] 240 | 241 | [[package]] 242 | name = "pathspec" 243 | version = "0.12.1" 244 | source = { registry = "https://pypi.org/simple" } 245 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 246 | wheels = [ 247 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 248 | ] 249 | 250 | [[package]] 251 | name = "platformdirs" 252 | version = "4.3.7" 253 | source = { registry = "https://pypi.org/simple" } 254 | sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } 255 | wheels = [ 256 | { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, 257 | ] 258 | 259 | [[package]] 260 | name = "pluggy" 261 | version = "1.5.0" 262 | source = { registry = "https://pypi.org/simple" } 263 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 264 | wheels = [ 265 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 266 | ] 267 | 268 | [[package]] 269 | name = "pydantic" 270 | version = "2.11.4" 271 | source = { registry = "https://pypi.org/simple" } 272 | dependencies = [ 273 | { name = "annotated-types" }, 274 | { name = "pydantic-core" }, 275 | { name = "typing-extensions" }, 276 | { name = "typing-inspection" }, 277 | ] 278 | sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } 279 | wheels = [ 280 | { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, 281 | ] 282 | 283 | [[package]] 284 | name = "pydantic-core" 285 | version = "2.33.2" 286 | source = { registry = "https://pypi.org/simple" } 287 | dependencies = [ 288 | { name = "typing-extensions" }, 289 | ] 290 | sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } 291 | wheels = [ 292 | { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, 293 | { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, 294 | { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, 295 | { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, 296 | { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, 297 | { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, 298 | { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, 299 | { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, 300 | { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, 301 | { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, 302 | { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, 303 | { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, 304 | { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, 305 | { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, 306 | { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, 307 | { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, 308 | { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, 309 | ] 310 | 311 | [[package]] 312 | name = "pytest" 313 | version = "8.3.5" 314 | source = { registry = "https://pypi.org/simple" } 315 | dependencies = [ 316 | { name = "colorama", marker = "sys_platform == 'win32'" }, 317 | { name = "iniconfig" }, 318 | { name = "packaging" }, 319 | { name = "pluggy" }, 320 | ] 321 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } 322 | wheels = [ 323 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, 324 | ] 325 | 326 | [[package]] 327 | name = "python-dateutil" 328 | version = "2.9.0.post0" 329 | source = { registry = "https://pypi.org/simple" } 330 | dependencies = [ 331 | { name = "six" }, 332 | ] 333 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } 334 | wheels = [ 335 | { 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, upload-time = "2024-03-01T18:36:18.57Z" }, 336 | ] 337 | 338 | [[package]] 339 | name = "pyyaml" 340 | version = "6.0.2" 341 | source = { registry = "https://pypi.org/simple" } 342 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } 343 | wheels = [ 344 | { 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, upload-time = "2024-08-06T20:32:43.4Z" }, 345 | { 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, upload-time = "2024-08-06T20:32:44.801Z" }, 346 | { 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, upload-time = "2024-08-06T20:32:46.432Z" }, 347 | { 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, upload-time = "2024-08-06T20:32:51.188Z" }, 348 | { 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, upload-time = "2024-08-06T20:32:53.019Z" }, 349 | { 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, upload-time = "2024-08-06T20:32:54.708Z" }, 350 | { 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, upload-time = "2024-08-06T20:32:56.985Z" }, 351 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, 352 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, 353 | ] 354 | 355 | [[package]] 356 | name = "pyyaml-env-tag" 357 | version = "0.1" 358 | source = { registry = "https://pypi.org/simple" } 359 | dependencies = [ 360 | { name = "pyyaml" }, 361 | ] 362 | sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload-time = "2020-11-12T02:38:26.239Z" } 363 | wheels = [ 364 | { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload-time = "2020-11-12T02:38:24.638Z" }, 365 | ] 366 | 367 | [[package]] 368 | name = "six" 369 | version = "1.17.0" 370 | source = { registry = "https://pypi.org/simple" } 371 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } 372 | wheels = [ 373 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, 374 | ] 375 | 376 | [[package]] 377 | name = "typing-extensions" 378 | version = "4.13.2" 379 | source = { registry = "https://pypi.org/simple" } 380 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 381 | wheels = [ 382 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 383 | ] 384 | 385 | [[package]] 386 | name = "typing-inspection" 387 | version = "0.4.0" 388 | source = { registry = "https://pypi.org/simple" } 389 | dependencies = [ 390 | { name = "typing-extensions" }, 391 | ] 392 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } 393 | wheels = [ 394 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, 395 | ] 396 | 397 | [[package]] 398 | name = "watchdog" 399 | version = "6.0.0" 400 | source = { registry = "https://pypi.org/simple" } 401 | sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } 402 | wheels = [ 403 | { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, 404 | { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, 405 | { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, 406 | { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, 407 | { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, 408 | { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, 409 | { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, 410 | { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, 411 | { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, 412 | { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, 413 | { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, 414 | { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, 415 | { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, 416 | ] 417 | 418 | [[package]] 419 | name = "wcmatch" 420 | version = "10.0" 421 | source = { registry = "https://pypi.org/simple" } 422 | dependencies = [ 423 | { name = "bracex" }, 424 | ] 425 | sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578, upload-time = "2024-09-26T18:39:52.505Z" } 426 | wheels = [ 427 | { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347, upload-time = "2024-09-26T18:39:51.002Z" }, 428 | ] 429 | --------------------------------------------------------------------------------