pax_global_header00006660000000000000000000000064152611712530014515gustar00rootroot0000000000000052 comment=55634c617854a314c1b7d207f0b0d179960ac970 python-ezgb-0.2.1/000077500000000000000000000000001526117125300137635ustar00rootroot00000000000000python-ezgb-0.2.1/.b4-config000066400000000000000000000004451526117125300155370ustar00rootroot00000000000000[b4] midmask = https://lore.kernel.org/%s linkmask = https://patch.msgid.link/%s send-series-to = Kernel.org Tools send-series-cc = Konstantin Ryabitsev send-endpoint-web = https://lkml.kernel.org/_b4_submit send-prefixes = ezgb python-ezgb-0.2.1/.gitignore000066400000000000000000000004711526117125300157550ustar00rootroot00000000000000# Python __pycache__/ *.py[cod] *.egg-info/ *.egg dist/ build/ # Virtual environments .venv/ .venv-*/ venv/ # ty type-checker cache .ty_cache/ # Testing .pytest_cache/ .coverage htmlcov/ # Type checking / linting .mypy_cache/ .ruff_cache/ # Sphinx docs docs/_build/ # Editors *.swp *.swo *~ .idea/ .vscode/ python-ezgb-0.2.1/CHANGELOG.rst000066400000000000000000000066521526117125300160150ustar00rootroot00000000000000========= Changelog ========= All notable changes to ezgb are documented in this file. This project adheres to `Semantic Versioning `_. 0.2.1 (2026-10-06) ================== Changed ------- - The minimum supported Python version is now 3.10 (was 3.9). Python 3.9 reached end of life in October 2025, and the code now uses ``typing.TypeGuard``, which was added in 3.10. On Python 3.9, pip keeps installing 0.2.0. - Values decoded from git-bug JSON are now checked before use. Bug, operation and identity data with unexpected types fails early, at the point where it is read. - The git helpers now use ``subprocess.run()`` instead of ``subprocess.Popen``. There is no change in behaviour. Fixed ----- - An operation with a ``null`` title no longer sets the bug title to ``None``. The previous title is kept instead. - ``COPYING`` now has the current Free Software Foundation address. Development ----------- - Enabled strict pyright checks for the source code, without the earlier blanket exemptions for unknown types and private names. Decoded JSON now has precise ``JsonValue`` and ``JsonObject`` types and git objects are narrowed to their pygit2 classes. - Added the typed ``json_loads()`` and ``json_raw_decode()`` helpers, so the newer ty "unsound assignment" checks pass without suppressions. - Replaced the dynamic ``BugWriter`` test double with a typed subclass. - The ``ci-matrix.sh`` interpreter matrix now covers Python 3.10--3.14. - Documented ``uv sync`` and ``ci.sh`` as the way to run local checks, in the README and the contributor guide. - Updated the development tools in ``uv.lock``: pyright 1.1.414, ruff 0.16.10, mypy 2.4.0 and ty 0.0.84. 0.2.0 (2026-07-23) ================== Fixed ----- - Native bug summaries (``BugReader.build_bug_summary``) now resolve the creator's display name into ``author_name`` and track the latest operation timestamp in ``edited_at``, so they match the values produced by the git-bug CLI cache. Previously the native read path left ``author_name`` empty and ``edited_at`` at the Unix epoch. This was most visible right after a ``pull`` -- which invalidates the CLI cache and forces the native fallback -- where consumers such as b4's bug TUI showed blank submitters and sorted the list by an epoch "last activity" date. Only the creator identity (one per bug) is resolved here; per-operation identity resolution stays deferred to ``build_bug()``. Added ----- - The read-only Lua library now records ``edited_at`` (the latest timestamp across all operation packs) in its bug summaries, letting Lua consumers sort bugs by last activity instead of creation date. Development ----------- - Added ``ci.sh`` and ``ci-matrix.sh`` CI guard scripts. ``ci.sh`` runs formatting, linting, three type checkers (mypy, pyright, ty), and the test suite; ``ci-matrix.sh`` runs an import smoke check and the suite under every supported interpreter (3.9--3.14) via uv-managed Pythons. - Added ``pyright`` and ``ty`` to the dev dependency group and configured ruff-format, pyright, and ty in ``pyproject.toml``. - Fully type-annotated the test suite and resolved the remaining pyright and ty findings in ``src/``. - Reformatted the code base with ``ruff format`` and dropped unused asyncio pytest options. - Committed ``uv.lock`` for reproducible CI installs. 0.1.1 ===== - Initial changelog baseline. See the git history for changes in this and earlier releases. python-ezgb-0.2.1/CONTRIBUTING.md000066400000000000000000000053621526117125300162220ustar00rootroot00000000000000# Contributing to ezgb Thank you for your interest in contributing to ezgb! ## Source code The current source code can be found at: git clone https://git.kernel.org/pub/scm/utils/ezgb/ezgb.git ## Submitting contributions Contributions should be sent as patches via email to **tools@kernel.org**. Please include a clear description of your changes and the reasoning behind them. You can use [b4](https://b4.docs.kernel.org/) to prepare and send your patches: b4 prep b4 send If you are not familiar with the patch-by-email workflow, the kernel contributor guide has a good overview: ## Coding style - Python 3.10+, `from __future__ import annotations` in every module. - `%`-style string formatting to match the existing code. - Run `./ci.sh` before submitting. ## Development checks uv sync --all-groups ./ci.sh The CI script runs Ruff, ty, mypy, pyright, and pytest. The test suite includes unit tests (using real git objects in temporary repos via pygit2) and integration tests that exercise the full stack including the `git-bug` binary. ## Licensing This project is licensed under the [GNU General Public License, version 2 or later (GPL-2.0+)](COPYING). By submitting a contribution, you agree to license your work under this license. ## Developer Certificate of Origin All contributions must include a `Signed-off-by:` trailer in each commit message, certifying that you wrote or otherwise have the right to submit the code under the project's open source license. This follows the [Developer Certificate of Origin (DCO)](DCO) process used by the Linux kernel and many other open source projects. To add your sign-off, use `git commit -s` or manually append the following line to your commit message: Signed-off-by: Your Name ### AI-assisted contributions AI agents (LLMs, coding assistants, etc.) are NOT permitted to use `Signed-off-by:` or `Co-developed-by:` trailers, as they cannot certify the DCO. Additionally, the `Co-developed-by:` trailer format requires an email address and AI agents typically use bogus addresses that interfere with patch-based workflows where valid contact information is expected. When a contribution is made with AI assistance, the AI tool MUST be credited with an `Assisted-by:` trailer that includes the full model version identifier. For example: Signed-off-by: Your Name Assisted-by: claude-opus-4-6-20250925 The human contributor remains responsible for reviewing all AI-generated changes, ensuring correctness, and certifying the DCO via their own `Signed-off-by:` trailer. The machine does not have any agency and therefore you, the human, are the sole entity responsible for all decisions. python-ezgb-0.2.1/COPYING000066400000000000000000000431001526117125300150140ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Moe Ghoul, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. python-ezgb-0.2.1/DCO000066400000000000000000000026151526117125300143170ustar00rootroot00000000000000Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 1 Letterman Drive Suite D4700 San Francisco, CA, 94129 Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. python-ezgb-0.2.1/MANIFEST.in000066400000000000000000000001131526117125300155140ustar00rootroot00000000000000include COPYING README.md CONTRIBUTING.md DCO recursive-include tests *.py python-ezgb-0.2.1/README.md000066400000000000000000000040211526117125300152370ustar00rootroot00000000000000# ezgb: easy git-bug for Python A standalone Python library for working with [git-bug](https://github.com/git-bug/git-bug) repositories. It lets you list, create, query, and update bugs that are stored as native git objects -- no external database required. ## Features - **Fast reads** -- bug data is read directly from git objects via pygit2 (libgit2 bindings). Listing uses the git-bug CLI cache when available (~100ms for hundreds of bugs). - **Safe writes** -- all mutations go through the `git bug` CLI, which keeps Lamport clocks and the operation DAG consistent. - **Simple API** -- one main class (`GitBugRepo`) with plain Python dataclasses for bugs, comments, and identities. ## Quick example ```python from ezgb import GitBugRepo, Status repo = GitBugRepo('/path/to/repo') # List open bugs for bug in repo.list_bugs(status=Status.OPEN): print(bug.title) # Create a new bug bug = repo.create_bug('Login page is broken', 'Steps to reproduce...') # Add a comment repo.add_comment(bug.id, 'I can reproduce this on Firefox 130.') ``` ## Requirements - Python 3.10 or later - pygit2 (libgit2 Python bindings) - `git-bug` v0.10+ on your `$PATH` (needed for write operations and fast cached listing) ## Installation ```bash git clone https://git.kernel.org/pub/scm/utils/ezgb/ezgb.git cd ezgb pip install -e . ``` ## Documentation Full documentation is in the `docs/` directory. Build it with: ```bash pip install sphinx sphinx-rtd-theme make -C docs html ``` Then open `docs/_build/html/index.html` in your browser. ## Development checks ```bash uv sync --all-groups ./ci.sh ``` The CI script runs Ruff, ty, mypy, pyright, and pytest. The test suite includes unit tests (using real git objects in temporary repos via pygit2) and integration tests that exercise the full stack including the `git-bug` binary. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for details on submitting patches, coding style, and the Developer Certificate of Origin. ## License GPLv2 or later. See [COPYING](COPYING) for the full text. python-ezgb-0.2.1/ci-matrix.sh000077500000000000000000000036241526117125300162240ustar00rootroot00000000000000#!/usr/bin/env sh set -eu # Run an import smoke check and pytest under every interpreter version in the # supported range (see `requires-python` in pyproject.toml). This is not part # of ci.sh because materialising a venv per version is slow; invoke this # before releases and after changes that could depend on version-specific # runtime behaviour (import-time annotations, stdlib APIs, etc.). # # Missing interpreters are pulled automatically from uv's managed Python # cache (python-build-standalone), so no system packages or sudo are needed. # # Override the version list: PYTHONS="3.10 3.14" ./ci-matrix.sh PYTHONS="${PYTHONS:-3.10 3.11 3.12 3.13 3.14}" # Install any requested interpreters that are missing. This is an explicit # step because a dev may have set UV_PYTHON_DOWNLOADS=manual globally to # prevent `uv sync` from reaching out to the network mid-workflow; running # it once up front scopes the network access to this script. # shellcheck disable=SC2086 uv python install $PYTHONS # Collect failures so the run reports a complete matrix instead of bailing on # the first broken interpreter. failed="" for py in $PYTHONS; do printf '\n=== Python %s ===\n' "$py" # Each version gets its own project environment so switching interpreters # does not thrash the default .venv and each sync is incremental. UV_PROJECT_ENVIRONMENT=".venv-$py" export UV_PROJECT_ENVIRONMENT if ! uv sync --all-extras --all-groups --python "$py"; then failed="$failed $py(sync)" continue fi if ! uv run python -c 'import ezgb, sys; print("import ezgb OK on", sys.version.split()[0])'; then failed="$failed $py(import)" continue fi if ! uv run pytest --durations=20; then failed="$failed $py(pytest)" continue fi done if [ -n "$failed" ]; then printf '\nFAILURES:%s\n' "$failed" exit 1 fi printf '\nAll interpreters passed: %s\n' "$PYTHONS" python-ezgb-0.2.1/ci.sh000077500000000000000000000005441526117125300147200ustar00rootroot00000000000000#!/usr/bin/env sh set -eu # Install every extra and dependency group so the type checkers below can # resolve every import path (including pygit2, the libgit2 backend used for # all read operations). uv sync --all-extras --all-groups uv run ruff format --check uv run ruff check uv run ty check uv run mypy . uv run pyright uv run pytest --durations=20 python-ezgb-0.2.1/docs/000077500000000000000000000000001526117125300147135ustar00rootroot00000000000000python-ezgb-0.2.1/docs/Makefile000066400000000000000000000005001526117125300163460ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) python-ezgb-0.2.1/docs/_static/000077500000000000000000000000001526117125300163415ustar00rootroot00000000000000python-ezgb-0.2.1/docs/_static/.gitkeep000066400000000000000000000000001526117125300177600ustar00rootroot00000000000000python-ezgb-0.2.1/docs/api.rst000066400000000000000000000022171526117125300162200ustar00rootroot00000000000000API reference ============= .. contents:: On this page :local: :depth: 2 GitBugRepo ---------- .. autoclass:: ezgb.GitBugRepo :members: :undoc-members: Data classes ------------ Bug ^^^ .. autoclass:: ezgb.Bug :members: :undoc-members: BugSummary ^^^^^^^^^^ .. autoclass:: ezgb.BugSummary :members: :undoc-members: Comment ^^^^^^^ .. autoclass:: ezgb.Comment :members: :undoc-members: Identity ^^^^^^^^ .. autoclass:: ezgb.Identity :members: :undoc-members: Enums ----- Status ^^^^^^ .. autoclass:: ezgb.Status :members: :undoc-members: Exceptions ---------- All exceptions inherit from :class:`~ezgb.EzgbError`, so you can catch that base class if you want to handle any ezgb error in one place. .. autoclass:: ezgb.EzgbError :show-inheritance: .. autoclass:: ezgb.BugNotFoundError :show-inheritance: .. autoclass:: ezgb.AmbiguousBugIdError :show-inheritance: .. autoclass:: ezgb.IdentityNotFoundError :show-inheritance: .. autoclass:: ezgb.UnsupportedFormatError :show-inheritance: .. autoclass:: ezgb.GitError :show-inheritance: .. autoclass:: ezgb.CliError :show-inheritance: python-ezgb-0.2.1/docs/architecture.rst000066400000000000000000000173751526117125300201440ustar00rootroot00000000000000Architecture ============ This page explains how ezgb works under the hood. You do not need to read this to use the library, but it may help if you want to contribute or if you are debugging unexpected behaviour. .. contents:: On this page :local: :depth: 2 Design principle: read fast, write safe --------------------------------------- ezgb splits its work into two paths: - **Reads** go directly to git objects via pygit2 (libgit2 bindings). This avoids spawning subprocesses for every lookup and makes bulk operations fast. - **Listing** uses the git-bug CLI excerpt cache when available (~100ms for hundreds of bugs). Falls back to native reads when the CLI is not installed. - **Writes** go through the ``git bug`` CLI. The CLI maintains Lamport clocks and the operation DAG, so bypassing it would risk corrupting the data. Module layout ------------- Python:: src/ezgb/ __init__.py GitBugRepo facade and public API _models.py Dataclasses, enums, exceptions, constants _git.py Subprocess wrapper for git-bug CLI (write ops) _reader.py BugReader: reads and caches bug data via pygit2 _writer.py BugWriter: delegates mutations to the git-bug CLI Lua:: lua/ ezgb.lua Read-only library (ref enumeration, op-pack replay, identity resolution, combined ID generation) How git-bug stores data ----------------------- git-bug keeps all its data inside the git repository itself, under special ref namespaces: - ``refs/bugs/`` -- one ref per bug, where ```` is a 64-character SHA-256 hex string. - ``refs/identities/`` -- one ref per user identity. Each ref points to a chain of commits. Every commit in the chain contains a tree with two entries: - ``ops`` -- a JSON blob holding an *operation pack* (a list of operations by one author). - ``version-N`` -- an empty blob that records the format version (currently ``version-4`` for bugs). Identity commits use the same structure, but the blob is called ``version`` (a flat JSON object, format version 2). Operation types ^^^^^^^^^^^^^^^ Bugs are built by replaying operations in order. Each operation has an integer ``type`` field: ==== ==================== =========================================== Type Name What it does ==== ==================== =========================================== 1 ``OP_CREATE`` Creates the bug with a title and message 2 ``OP_SET_TITLE`` Changes the title 3 ``OP_ADD_COMMENT`` Adds a new comment 4 ``OP_SET_STATUS`` Sets the status to open (1) or closed (2) 5 ``OP_LABEL_CHANGE`` Adds or removes labels 6 ``OP_EDIT_COMMENT`` Edits an existing comment's text 7 ``OP_NOOP`` No-op, used for bridge metadata 8 ``OP_SET_METADATA`` Attaches key-value metadata ==== ==================== =========================================== How reading works ----------------- When you call ``repo.get_bug(bid)``, the following happens: 1. **Resolve the ID.** If ``bid`` is a short prefix, ezgb iterates ``refs/bugs/`` references via pygit2 to find the matching full ID. 2. **Walk the commit chain.** pygit2 follows the commit parent chain from the ref tip back to the root, collecting the ``ops`` blob from each commit's tree (oldest first). 3. **Read the blobs.** Each ``ops`` blob is read directly from the repository's object database via pygit2 -- no subprocess overhead. 4. **Replay the operations.** Each operation pack is parsed as JSON and its operations are applied in sequence to build a :class:`~ezgb.Bug` snapshot. 5. **Cache the result.** The finished ``Bug`` object is stored in an in-memory dict, keyed by the full bug ID. Subsequent reads for the same bug return the cached object immediately. Identity resolution follows a similar pattern: read the latest ``version`` blob from the identity's commit chain and parse it. Fast listing with the CLI cache ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :meth:`~ezgb.GitBugRepo.list_bug_summaries` transparently uses the git-bug CLI's pre-built excerpt cache when available. This runs ``git bug -f json`` which reads the gob-encoded cache at ``.git/git-bug/cache/bugs`` and returns all bug metadata in ~100ms, regardless of repository size. The method falls back to native pygit2 reads when the CLI is not installed or when a ``since`` filter is used. How writing works ----------------- All write methods go through the same path: 1. Run ``git -C bug ...`` via ``subprocess``. 2. Check the exit code. If non-zero, raise :class:`~ezgb.CliError`. 3. Invalidate the reader's cache for the affected bug (or all bugs, in the case of ``create_bug``). 4. Re-read the bug from git objects (via pygit2) so the caller gets an up-to-date snapshot. This means the ``git-bug`` binary must be installed for any write operation. Caching ------- Four in-memory caches are maintained: - **Bug cache** -- maps full bug ID to a ``Bug`` object. - **Summary cache** -- maps full bug ID to a ``BugSummary`` object. - **Identity cache** -- maps identity ID to an ``Identity`` object. - **Resolve cache** -- maps bug ID prefixes (and full IDs) to full IDs. Call :meth:`~ezgb.GitBugRepo.invalidate` to clear them. Passing a specific bug ID only evicts that bug from the bug and summary caches. Passing no argument clears all four caches. Write operations automatically invalidate the affected bug, so you normally do not need to call ``invalidate()`` yourself. The main use case is when the repository changes externally (for example after a ``git fetch`` or ``git pull``). Comment IDs ----------- git-bug identifies comments using *combined IDs* -- a 64-character string formed by interleaving the bug ID and the operation hash. This gives each comment a globally unique, deterministic identifier. The interleaving pattern places the operation hash characters at positions 1, 3, 5, 9, and then every position where ``i >= 10 and i % 5 == 4``. All other positions come from the bug ID. Operation hashing ^^^^^^^^^^^^^^^^^ git-bug computes each operation's hash by SHA-256-hashing the compact JSON serialization produced by Go's ``json.Marshal``. Go escapes ``<``, ``>``, and ``&`` as ``\u003c``, ``\u003e``, ``\u0026`` -- a Go-specific behaviour that neither Python's ``json.dumps`` nor Lua's cjson reproduces. Re-serializing a parsed operation would produce different bytes and a different hash, which breaks ``OP_EDIT_COMMENT`` matching (the ``target`` field carries the Go-computed hash). To get correct hashes, both the Python and Lua implementations extract each operation's raw JSON string directly from the blob, preserving Go's escaping verbatim. In Python this is done using ``json.JSONDecoder.raw_decode()`` to find object boundaries in the original string. In Lua, a manual brace-depth tracker serves the same purpose. Lua library ----------- The Lua library (``lua/ezgb.lua``) mirrors the Python reader's functionality but is read-only -- it has no write path. Instead of spawning ``git`` subprocesses, it uses `luagit2 `_ (libgit2 bindings) for direct git object access. This makes it suitable for embedding in tools like cgit where subprocess overhead would be unacceptable. The public API returns plain Lua tables: .. code-block:: lua local ezgb = require("ezgb") ezgb.open("/path/to/repo") -- List bugs local refs = ezgb.list_bug_refs() -- Build a full bug snapshot local bug = ezgb.build_bug(refs[1].id) -- bug.title, bug.status, bug.creator, bug.comments, ... -- Resolve identities local id = ezgb.resolve_identity(identity_id) -- id.name, id.email, id.login Labels are stored as set-tables (``{["label"] = true, ...}``) rather than arrays. Timestamps are plain unix integers. Error handling uses Lua's idiomatic ``nil, error_string`` return pattern. python-ezgb-0.2.1/docs/conf.py000066400000000000000000000025371526117125300162210ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0-or-later # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys # Make the package importable for autodoc sys.path.insert(0, os.path.abspath(os.path.join('..', 'src'))) # -- Project information ----------------------------------------------------- project = 'ezgb' copyright = '2024-2026, The Linux Foundation' author = 'Konstantin Ryabitsev' release = '0.2.1' # -- General configuration --------------------------------------------------- extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', ] templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] html_theme_options = { 'navigation_depth': 3, 'collapse_navigation': False, } # -- autodoc configuration --------------------------------------------------- autodoc_member_order = 'bysource' autodoc_typehints = 'description' # -- intersphinx configuration ----------------------------------------------- intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } python-ezgb-0.2.1/docs/index.rst000066400000000000000000000032141526117125300165540ustar00rootroot00000000000000ezgb: easy git-bug =================== **ezgb** is a standalone library for working with `git-bug `_ repositories. It lets you list, create, query, and update bugs that are stored as native git objects -- no external database required. Both Python and Lua implementations are provided. The Python library supports reading and writing; the Lua library is read-only and is designed for embedding in tools like `cgit `_. .. code-block:: python from ezgb import GitBugRepo, Status repo = GitBugRepo('/path/to/repo') # List open bugs for bug in repo.list_bugs(status=Status.OPEN): print(bug.title) # Create a new bug bug = repo.create_bug('Login page is broken', 'Steps to reproduce...') # Add a comment repo.add_comment(bug.id, 'I can reproduce this on Firefox 130.') .. code-block:: lua local ezgb = require("ezgb") ezgb.open("/path/to/repo") for _, ref in ipairs(ezgb.list_bug_refs()) do local bug = ezgb.build_bug(ref.id) print(bug.title) end Key features ------------ - **Fast reads** -- bug data is read directly from git objects (via pygit2 in Python, via luagit2 in Lua), so listing hundreds of bugs is quick. - **Safe writes** (Python only) -- all mutations go through the ``git bug`` CLI, which keeps Lamport clocks and the operation DAG consistent. - **Simple API** -- one main class in Python (``GitBugRepo``), one module in Lua (``ezgb``), with plain data structures for bugs, comments, and identities. .. toctree:: :maxdepth: 2 :caption: Contents installation quickstart api architecture releases python-ezgb-0.2.1/docs/installation.rst000066400000000000000000000036341526117125300201540ustar00rootroot00000000000000Installation ============ .. contents:: On this page :local: :depth: 2 Python library -------------- Requirements ^^^^^^^^^^^^ - **Python 3.10** or later. - **pygit2** -- Python bindings for libgit2 (``pip install pygit2``). - **git-bug** (v0.10 or later) must be on your ``$PATH`` if you want to create or modify bugs, or to use the fast CLI-cache listing path. Reading existing bugs only needs pygit2. Install from source ^^^^^^^^^^^^^^^^^^^ Clone the repository and install in editable mode: .. code-block:: bash git clone https://git.kernel.org/pub/scm/utils/ezgb/ezgb.git cd ezgb pip install -e . To also install the development tools (pytest, mypy, ruff, pyright, and ty), which live in a PEP 735 ``[dependency-groups]`` table: .. code-block:: bash # with uv (installs the project and the dev tools) uv sync # or with pip 25.1 or later pip install -e . --group dev Verify ^^^^^^ Open a Python shell and check that the import works: .. code-block:: python >>> import ezgb >>> ezgb.GitBugRepo If you plan to create or modify bugs, make sure ``git-bug`` is available: .. code-block:: bash $ git bug version git-bug version: v0.10.1 Lua library ----------- The Lua library is read-only and uses luagit2 for git object access. Requirements ^^^^^^^^^^^^ - **Lua 5.1** or later (including LuaJIT). - **luagit2** (``lua-git2``) -- libgit2 bindings for Lua. - **luaossl** -- OpenSSL bindings (for SHA-256 operation hashing). - **lua-cjson** or **lua-json** -- JSON parsing. Install with luarocks ^^^^^^^^^^^^^^^^^^^^^ From the ezgb source tree: .. code-block:: bash cd ezgb luarocks make lua-ezgb-scm-1.rockspec This installs ``ezgb.lua`` to the standard Lua module path (typically ``/usr/share/lua/5.4/`` or ``~/.luarocks/share/lua/5.4/``). Verify ^^^^^^ .. code-block:: console $ lua -e 'local ezgb = require("ezgb"); print("ok")' ok python-ezgb-0.2.1/docs/quickstart.rst000066400000000000000000000125111526117125300176370ustar00rootroot00000000000000Quick start =========== This page walks through the most common tasks. Every example assumes you already have a git repository with git-bug set up. .. contents:: On this page :local: :depth: 2 Opening a repository -------------------- Point ``GitBugRepo`` at any git working tree or bare repository that contains git-bug data: .. code-block:: python from ezgb import GitBugRepo repo = GitBugRepo('/srv/git/myproject.git') Listing bugs ------------ Call :meth:`~ezgb.GitBugRepo.list_bugs` to get every bug, or pass filters: .. code-block:: python from ezgb import Status # All bugs all_bugs = repo.list_bugs() # Only open bugs open_bugs = repo.list_bugs(status=Status.OPEN) # Only bugs with a specific label net_bugs = repo.list_bugs(label='area/network') # Only bugs modified after a given time recent = repo.list_bugs(since='2024-11-01 00:00:00') Each item in the list is a :class:`~ezgb.Bug` dataclass. The *since* parameter accepts a unix timestamp (``int``), a :class:`~datetime.datetime`, or a date string in ISO-8601 / ``YYYYMMDDHHMMSS`` format. Iterating lazily ---------------- For large repositories, :meth:`~ezgb.GitBugRepo.iter_bugs` yields bugs one at a time instead of loading them all into memory: .. code-block:: python for bug in repo.iter_bugs(status=Status.OPEN): print(bug.title) It accepts the same filters as ``list_bugs``. Reading a single bug -------------------- If you know the bug ID (or an unambiguous prefix), use :meth:`~ezgb.GitBugRepo.get_bug`: .. code-block:: python bug = repo.get_bug('415b4497') print(bug.title) # 'Login page is broken' print(bug.status) # Status.OPEN print(bug.creator.name) # 'Alice' print(bug.labels) # {'area/web', 'priority/high'} for comment in bug.comments: print(comment.author.name, comment.text) Creating a bug -------------- :meth:`~ezgb.GitBugRepo.create_bug` returns the new bug as a :class:`~ezgb.Bug`: .. code-block:: python bug = repo.create_bug( title='Crash on startup', body='The application crashes when the config file is missing.', ) print(bug.id) # full 64-character hex ID .. note:: Write operations need the ``git bug`` CLI on your ``$PATH``. Lightweight summaries for list views ------------------------------------ For list views where you don't need full comment data, use :meth:`~ezgb.GitBugRepo.list_bug_summaries`. This is much faster than ``list_bugs`` because it uses the git-bug CLI cache when available (~100ms vs seconds for large repositories): .. code-block:: python summaries = repo.list_bug_summaries(status=Status.OPEN) for s in summaries: print(f'{s.id[:7]} {s.author_name} {s.title}') print(f' {s.comment_count} comments, last changed {s.edited_at}') Each item is a :class:`~ezgb.BugSummary` dataclass with ``id``, ``title``, ``status``, ``labels``, ``created_at``, ``edited_at``, ``author_name``, and ``comment_count``. Adding comments --------------- .. code-block:: python comment = repo.add_comment(bug.id, 'I can reproduce this on Fedora 41.') print(comment.count) # 1 (the first comment after the description) Editing comments ---------------- Use :meth:`~ezgb.GitBugRepo.edit_comment` to replace a comment's text. This is useful for tombstoning comments (data removal) while preserving the comment's identity in the operation history: .. code-block:: python # Replace a comment's text repo.edit_comment(bug.id, comment.id, 'Updated text') # Tombstone a comment (remove content but keep the ID) repo.edit_comment(bug.id, comment.id, 'X-B4-Bug-Comment: removed') Changing status --------------- .. code-block:: python repo.set_status(bug.id, Status.CLOSED) # Later, reopen it: repo.set_status(bug.id, Status.OPEN) Editing the title ----------------- .. code-block:: python repo.set_title(bug.id, 'Crash on startup when config is missing') Working with labels ------------------- .. code-block:: python repo.add_label(bug.id, 'priority/high') repo.remove_label(bug.id, 'priority/high') Removing a bug -------------- To permanently delete a bug (this is irreversible): .. code-block:: python repo.remove_bug(bug.id) Syncing with a remote --------------------- Push and pull git-bug data to/from a remote: .. code-block:: python # Push bugs and identities returncode, stdout, stderr = repo.push('origin') # Pull and merge (caches are invalidated automatically) returncode, stdout, stderr = repo.pull('origin') Reading attachments ------------------- Comments can reference file attachments by blob hash. Use :meth:`~ezgb.GitBugRepo.get_attachment` to read the raw bytes: .. code-block:: python for comment in bug.comments: for blob_hash in comment.attachment_ids: data = repo.get_attachment(blob_hash) print(len(data), 'bytes') Listing identities ------------------ Every person who has interacted with git-bug in the repository has an identity: .. code-block:: python for identity in repo.list_identities(): print(identity.name, identity.email) Caching ------- ezgb caches bug snapshots in memory after the first read. If you know the repository has changed (for example after a ``git pull``), clear the cache: .. code-block:: python # Clear everything repo.invalidate() # Clear just one bug repo.invalidate(bug.id) python-ezgb-0.2.1/docs/releases.rst000066400000000000000000000043451526117125300172560ustar00rootroot00000000000000Release notes ============= A high-level overview of what changed in each ezgb release. For the full, itemised list -- including development and packaging changes -- see the ``CHANGELOG.rst`` file at the root of the source tree. .. contents:: On this page :local: :depth: 1 0.2.1 (2026-10-06) ------------------ This is a maintenance release. ezgb now needs **Python 3.10 or later**. Python 3.9 reached end of life in October 2025. If you still use it, pip keeps installing 0.2.0 for you. The library now checks the JSON data that it reads from git-bug before using it, so malformed bug data fails early. An operation with a ``null`` title no longer clears the bug title. The public API does not change. Under the hood, the whole code base now passes strict type checking with pyright, mypy and ty, and the ``COPYING`` file has the current Free Software Foundation address. 0.2.0 (2026-07-23) ------------------ Bug summaries gained consistent author and activity metadata. Native summaries built by :meth:`~ezgb.GitBugRepo.list_bug_summaries` (the pygit2 read path) now populate ``author_name`` with the creator's resolved display name and ``edited_at`` with the latest operation timestamp, matching the summaries produced from the git-bug CLI cache. Previously the native path left these blank and at the Unix epoch, which was most noticeable right after a ``pull`` -- tools that list bugs, such as b4's bug TUI, showed empty submitters and sorted by an epoch date. The read-only Lua library likewise now records ``edited_at`` in its bug summaries, so Lua consumers can sort by last activity. Development-wise, ezgb picked up a full CI guard suite -- code formatting, linting, three type checkers (mypy, pyright, and ty), and a test matrix spanning every supported interpreter (Python 3.9--3.14) -- together with a fully type-annotated test suite and a pinned ``uv.lock`` for reproducible installs. 0.1.1 and earlier ----------------- The initial development releases established the foundations: a Python read/write library and a read-only Lua port, native git-object reads via pygit2 (and luagit2 in Lua), the fast git-bug CLI-cache listing path, and the core bug, comment, and identity model. See the git history for the details of these early releases. python-ezgb-0.2.1/docs/requirements.txt000066400000000000000000000000421526117125300201730ustar00rootroot00000000000000sphinx>=7.0 sphinx-rtd-theme>=2.0 python-ezgb-0.2.1/lua-ezgb-scm-1.rockspec000066400000000000000000000012331526117125300201410ustar00rootroot00000000000000rockspec_format = "3.0" package = "lua-ezgb" version = "scm-1" source = { url = "git+https://git.kernel.org/pub/scm/utils/ezgb/ezgb.git", } description = { summary = "Read-only library for git-bug repositories", detailed = [[ ezgb.lua reads bug and identity data from git-bug repositories by parsing git objects directly via luagit2. Designed for use inside cgit's Lua filter system, but works standalone too. ]], homepage = "https://git.kernel.org/pub/scm/utils/ezgb/ezgb.git", license = "GPL-2.0-or-later", } dependencies = { "lua >= 5.1", } build = { type = "builtin", modules = { ezgb = "lua/ezgb.lua", }, } python-ezgb-0.2.1/lua/000077500000000000000000000000001526117125300145445ustar00rootroot00000000000000python-ezgb-0.2.1/lua/README.md000066400000000000000000000045471526117125300160350ustar00rootroot00000000000000# ezgb.lua -- read-only Lua library for git-bug repositories ezgb.lua reads bug and identity data directly from git-bug's git object storage using luagit2 (libgit2 bindings). It is read-only: creating, modifying, or deleting bugs requires the Python library or the git-bug CLI. The primary use case is embedding in tools like cgit filters where subprocess overhead is unacceptable and only read access is needed. ## Dependencies - **luagit2** (`lua-git2`) -- libgit2 bindings for Lua - **luaossl** -- OpenSSL bindings (SHA-256 for operation hashing) - **lua-cjson** or **lua-json** -- JSON parsing ## Installation From the ezgb source tree: ```bash luarocks make lua-ezgb-scm-1.rockspec ``` ## Usage ```lua local ezgb = require("ezgb") -- Open a repository (bare or non-bare) ezgb.open("/path/to/repo") -- List all bug refs local refs = ezgb.list_bug_refs() for _, ref in ipairs(refs) do print(ref.id, ref.commit) end -- Build a full bug snapshot local bug = ezgb.build_bug(refs[1].id) print(bug.id, bug.title, bug.status) print("comments:", #bug.comments) print("labels:", bug.labels) -- set-table: {["label"] = true, ...} print("creator:", bug.creator.name, bug.creator.email) -- Resolve an identity by ID local identity = ezgb.resolve_identity(identity_id) print(identity.name, identity.email) -- Resolve a short bug ID to its full 64-char hex ID local full_id = ezgb.resolve_bug_id("abc1234", refs) -- Clear cached data ezgb.clear_cache() ``` ## Data format Bug snapshots are plain Lua tables: - `bug.id` -- 64-character hex string - `bug.title` -- current title (after replaying all operations) - `bug.status` -- `1` (open) or `2` (closed) - `bug.creator` -- table with `id`, `name`, `email`, `login` - `bug.created_at` -- unix timestamp - `bug.labels` -- set-table (`{["label_name"] = true, ...}`) - `bug.comments` -- array of comment tables - `bug.metadata` -- key-value table Each comment table contains `id`, `author`, `text`, `created_at`, `count`, and `attachment_ids`. ## Differences from the Python library | Feature | Python (ezgb) | Lua (ezgb.lua) | |---------|---------------|-----------------| | Git access | pygit2 (libgit2) | luagit2 (libgit2) | | Write operations | Yes (via git-bug CLI) | No | | CLI cache listing | Yes | No | | In-memory caching | Yes | Yes | | Module state | Instance-based (GitBugRepo) | Module-level global | ## License GPL-2.0-or-later python-ezgb-0.2.1/lua/ezgb.lua000066400000000000000000000541721526117125300162070ustar00rootroot00000000000000-- SPDX-License-Identifier: GPL-2.0-or-later -- Copyright (C) 2026 by the Linux Foundation -- -- ezgb.lua -- read-only library for git-bug repositories -- -- Reads bug and identity data from git-bug's git object storage -- using luagit2 (libgit2 bindings) for git access. -- -- Dependencies: -- luagit2 (git object access via libgit2) -- lua-cjson or lua-json (JSON parsing) -- luaossl (SHA-256 for operation hashing) -- ── Dependencies ────────────────────────────────────────────────────── local ok, json = pcall(require, "cjson") if not ok then ok, json = pcall(require, "json") if not ok then error("ezgb: requires lua-cjson or lua-json") end end local digest = require("openssl.digest") local M = {} -- ── Constants ───────────────────────────────────────────────────────── M.OP_CREATE = 1 M.OP_SET_TITLE = 2 M.OP_ADD_COMMENT = 3 M.OP_SET_STATUS = 4 M.OP_LABEL_CHANGE = 5 M.OP_EDIT_COMMENT = 6 M.OP_NOOP = 7 M.OP_SET_METADATA = 8 M.STATUS_OPEN = 1 M.STATUS_CLOSED = 2 M.SUPPORTED_BUG_FORMAT = 4 M.SUPPORTED_IDENTITY_FORMAT = 2 local ID_LENGTH = 64 -- Detect the JSON module's null sentinel by decoding an actual null. -- cjson uses a userdata, lua-json uses a function, others vary. local json_null do local decoded = json.decode("[null]") json_null = decoded[1] end -- ── Helpers ─────────────────────────────────────────────────────────── -- Return v if it is a real value, otherwise return default. -- Treats cjson.null as absent. local function jval(v, default) if v == nil or v == json_null then return default end return v end -- SHA-256 hex digest local function sha256_hex(data) local b = digest.new("sha256"):final(data) local hex = "" for i = 1, #b do hex = hex .. string.format("%02x", string.byte(b, i)) end return hex end -- ── Git access via luagit2 ───────────────────────────────────────────── local git2 = require("git2") -- Repository handle, set by M.open() local repo = nil function M.open(path) local r, err = git2.Repository(path) if not r then error("ezgb: cannot open repository: " .. tostring(path)) end repo = r end function M._git_for_each_ref(pattern) local ref_names = git2.Reference.list(repo) if not ref_names then return {} end local refs = {} for i = 0, ref_names:count() - 1 do local name = ref_names:str(i) if name:sub(1, #pattern) == pattern then local ref = git2.Reference.lookup(repo, name) if ref then local oid = ref:target() local date = 0 if oid then local commit = git2.Commit.lookup(repo, oid) if commit then date = commit:time() end end refs[#refs + 1] = { name = name, oid = oid and oid:fmt() or "", date = date, } end end end return refs end function M._git_read_blob(oid_hex) local oid = git2.OID.hex(oid_hex) if not oid then return nil end local blob = git2.Blob.lookup(repo, oid) if not blob then return nil end local content = blob:rawcontent() return content end function M._git_tree_entries(tree_oid_hex) local oid = git2.OID.hex(tree_oid_hex) if not oid then return nil end local tree = git2.Tree.lookup(repo, oid) if not tree then return nil end local entries = {} for i = 0, tree:entrycount() - 1 do local entry = tree:entry_byindex(i) if entry then entries[#entries + 1] = { name = entry:name(), oid = entry:id():fmt(), mode = entry:filemode(), } end end return entries end function M._git_commit_log(ref) local ref_obj = git2.Reference.lookup(repo, ref) if not ref_obj then return {} end local target = ref_obj:target() if not target then return {} end local revwalk = git2.RevWalk.new(repo) -- SORT_TOPOLOGICAL (1) + SORT_REVERSE (4) = oldest first revwalk:sorting(5) revwalk:push(target) local commits = {} local commit_oid = revwalk:next() while commit_oid do local commit = git2.Commit.lookup(repo, commit_oid) if commit then local tree = commit:tree() if tree then commits[#commits + 1] = { oid = commit_oid:fmt(), tree_oid = tree:id():fmt(), } end end commit_oid = revwalk:next() end return commits end -- ── Raw op extraction ───────────────────────────────────────────────── -- -- git-bug identifies each operation by SHA-256-hashing its JSON -- serialization. That serialization is produced by Go's json.Marshal, -- which escapes <, >, & as \u003c, \u003e, \u0026. Neither Lua's -- cjson nor Python's json.dumps reproduces those escapes, so -- re-encoding a decoded op produces different bytes and a wrong hash -- (breaking OP_EDIT_COMMENT matching, where the "target" field carries -- the Go-computed hash). -- -- To get correct hashes we extract each op's raw JSON string directly -- from the blob, preserving Go's escaping verbatim. Unlike Python, -- Lua has no raw_decode equivalent, so we track brace depth and string -- boundaries manually. local function extract_raw_ops(blob_json) local ops_start = blob_json:find('"ops"%s*:%s*%[') if not ops_start then return {} end local bracket_pos = blob_json:find('%[', ops_start) if not bracket_pos then return {} end local pos = bracket_pos + 1 local ops = {} local len = #blob_json while pos <= len do -- skip whitespace and commas local ws_end = blob_json:match("^[%s,]*()", pos) if ws_end then pos = ws_end end if pos > len then break end local c = blob_json:byte(pos) if c == 0x5D then break end -- ']' = end of ops array if c == 0x7B then -- '{' = start of op object local depth = 0 local in_str = false local start = pos while pos <= len do c = blob_json:byte(pos) if in_str then if c == 0x5C then -- '\' = escape next char pos = pos + 1 elseif c == 0x22 then -- '"' = end of string in_str = false end else if c == 0x22 then -- '"' in_str = true elseif c == 0x7B then -- '{' depth = depth + 1 elseif c == 0x7D then -- '}' depth = depth - 1 if depth == 0 then ops[#ops + 1] = blob_json:sub(start, pos) pos = pos + 1 break end end end pos = pos + 1 end else pos = pos + 1 end end return ops end -- ── Combined IDs ────────────────────────────────────────────────────── -- -- Mirrors git-bug's CombineIds: interleaves primary (bug) and secondary -- (op hash) into a single 64-char ID. -- Pattern: PSPSPSPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPP function M.combine_ids(primary, secondary) local result = {} local pi, si = 1, 1 for i = 0, ID_LENGTH - 1 do if i == 1 or i == 3 or i == 5 or i == 9 or (i >= 10 and i % 5 == 4) then result[#result + 1] = secondary:sub(si, si) si = si + 1 else result[#result + 1] = primary:sub(pi, pi) pi = pi + 1 end end return table.concat(result) end -- ── Operation hashing ───────────────────────────────────────────────── function M.op_hash(raw_op_json) return sha256_hex(raw_op_json) end -- ── Format version checking ────────────────────────────────────────── local function check_format_version(entries, prefix, supported) for _, e in ipairs(entries) do local ver_str = e.name:match("^" .. prefix .. "(%d+)$") if ver_str then local ver = tonumber(ver_str) if ver ~= supported then return nil, string.format( "unsupported format: %s%d (expected %s%d)", prefix, ver, prefix, supported ) end return true end end return true -- no version entry found, assume OK end -- ── Identity cache ──────────────────────────────────────────────────── local identity_cache = {} local function fallback_identity(iid) return {id = iid, name = iid, email = iid, login = ""} end -- ── Ref enumeration ─────────────────────────────────────────────────── function M.list_bug_refs(since) since = since or 0 local refs = M._git_for_each_ref("refs/bugs/") local result = {} for _, ref in ipairs(refs) do if since <= 0 or (ref.date and ref.date >= since) then local bid = ref.name:match("([^/]+)$") if bid then result[#result + 1] = { id = bid, oid = ref.oid, date = ref.date, } end end end return result end function M.list_identity_refs() local refs = M._git_for_each_ref("refs/identities/") local result = {} for _, ref in ipairs(refs) do local iid = ref.name:match("([^/]+)$") if iid then result[#result + 1] = {id = iid, oid = ref.oid} end end return result end -- ── Bug ID resolution ───────────────────────────────────────────────── function M.resolve_bug_id(bid, refs) if #bid == ID_LENGTH then return bid end if not refs then refs = M.list_bug_refs() end local candidates = {} for _, ref in ipairs(refs) do if ref.id:sub(1, #bid) == bid then candidates[#candidates + 1] = ref.id end end if #candidates == 1 then return candidates[1] end if #candidates > 1 then return nil, string.format( "bug ID prefix %s matches %d bugs", bid, #candidates ) end return nil, string.format("no bug matching ID prefix %s", bid) end -- ── Identity resolution ─────────────────────────────────────────────── function M.resolve_identity(iid) if identity_cache[iid] then return identity_cache[iid] end local commits = M._git_commit_log("refs/identities/" .. iid) if not commits or #commits == 0 then local fb = fallback_identity(iid) identity_cache[iid] = fb return fb end -- Latest commit is last in oldest-first order local latest = commits[#commits] local entries = M._git_tree_entries(latest.tree_oid) if not entries then local fb = fallback_identity(iid) identity_cache[iid] = fb return fb end -- Find the 'version' blob (identity data, not 'ops') local version_oid for _, e in ipairs(entries) do if e.name == "version" then version_oid = e.oid break end end if not version_oid then local fb = fallback_identity(iid) identity_cache[iid] = fb return fb end local raw = M._git_read_blob(version_oid) if not raw then local fb = fallback_identity(iid) identity_cache[iid] = fb return fb end local decode_ok, data = pcall(json.decode, raw) if not decode_ok then local fb = fallback_identity(iid) identity_cache[iid] = fb return fb end -- Validate format version (stored in the JSON, not the filename) local fmt_ver = jval(data.version, nil) if fmt_ver ~= nil and fmt_ver ~= M.SUPPORTED_IDENTITY_FORMAT then local fb = fallback_identity(iid) identity_cache[iid] = fb return fb end local identity = { id = iid, name = jval(data.name, iid), email = jval(data.email, iid), login = jval(data.login, ""), } identity_cache[iid] = identity return identity end -- ── Op-pack reading ─────────────────────────────────────────────────── -- -- Walk the commit chain for a bug and return operation packs paired -- with their raw op JSON strings (needed for op_hash). local function get_op_packs(bid) local commits = M._git_commit_log("refs/bugs/" .. bid) if not commits or #commits == 0 then return nil, "no commits for bug " .. bid end local packs = {} local raw_ops_lists = {} for idx, c in ipairs(commits) do local entries = M._git_tree_entries(c.tree_oid) if entries then -- Check format version on the first commit's tree if idx == 1 then local ver_ok, err = check_format_version( entries, "version%-", M.SUPPORTED_BUG_FORMAT ) if not ver_ok then return nil, err end end -- Find and read the ops blob for _, e in ipairs(entries) do if e.name == "ops" then local raw = M._git_read_blob(e.oid) if raw then local decode_ok, pack = pcall(json.decode, raw) if decode_ok then packs[#packs + 1] = pack raw_ops_lists[#raw_ops_lists + 1] = extract_raw_ops(raw) end end break end end end end return packs, raw_ops_lists end -- ── Bug building (operation replay) ────────────────────────────────── function M.build_bug(bid) local full_id, err = M.resolve_bug_id(bid) if not full_id then return nil, err end local packs, raw_ops_lists = get_op_packs(full_id) if not packs then return nil, raw_ops_lists end -- error string if #packs == 0 then return nil, "no operation packs for bug " .. full_id end local title = "" local is_open = true local creator = nil local created_at = 0 local labels = {} local comments = {} local comment_count = 0 local metadata = {} -- Map op hashes to comment tables for OP_EDIT_COMMENT local op_hash_map = {} for pack_idx, pack in ipairs(packs) do local author_id = "" local author_tbl = jval(pack.author, nil) if author_tbl then author_id = jval(author_tbl.id, "") end local raw_ops = raw_ops_lists[pack_idx] or {} local ops = jval(pack.ops, {}) for op_idx, op in ipairs(ops) do local op_type = jval(op.type, 0) local timestamp = jval(op.timestamp, 0) local raw_json = raw_ops[op_idx] local op_id = raw_json and M.op_hash(raw_json) or "" if op_type == M.OP_CREATE then title = jval(op.title, "") local author = M.resolve_identity(author_id) creator = author created_at = timestamp local message = jval(op.message, "") if message ~= "" then local files = jval(op.files, {}) local combined_id = M.combine_ids(full_id, op_id) local cmt = { id = combined_id, author = author, text = message, created_at = timestamp, count = comment_count, attachment_ids = files, } comments[#comments + 1] = cmt op_hash_map[op_id] = cmt comment_count = comment_count + 1 end local op_meta = jval(op.metadata, nil) if op_meta then for k, v in pairs(op_meta) do metadata[k] = v end end elseif op_type == M.OP_SET_TITLE then title = jval(op.title, title) elseif op_type == M.OP_ADD_COMMENT then local message = jval(op.message, "") local files = jval(op.files, {}) local op_author = M.resolve_identity(author_id) local combined_id = M.combine_ids(full_id, op_id) local cmt = { id = combined_id, author = op_author, text = message, created_at = timestamp, count = comment_count, attachment_ids = files, } comments[#comments + 1] = cmt op_hash_map[op_id] = cmt comment_count = comment_count + 1 elseif op_type == M.OP_SET_STATUS then local status_val = jval(op.status, M.STATUS_OPEN) is_open = (status_val == M.STATUS_OPEN) elseif op_type == M.OP_LABEL_CHANGE then for _, lbl in ipairs(jval(op.added, {})) do labels[lbl] = true end for _, lbl in ipairs(jval(op.removed, {})) do labels[lbl] = nil end elseif op_type == M.OP_EDIT_COMMENT then local target = jval(op.target, "") local matched = op_hash_map[target] if matched then matched.text = jval(op.message, "") local new_files = jval(op.files, {}) if #new_files > 0 then matched.attachment_ids = new_files end end elseif op_type == M.OP_SET_METADATA then local new_meta = jval(op.new_metadata, nil) if new_meta then for k, v in pairs(new_meta) do metadata[k] = v end end -- OP_NOOP: nothing to do end end end if not creator then creator = fallback_identity("unknown") end return { id = full_id, title = title, status = is_open and M.STATUS_OPEN or M.STATUS_CLOSED, creator = creator, created_at = created_at, labels = labels, comments = comments, metadata = metadata, } end function M.build_bug_summary(bid) local full_id, err = M.resolve_bug_id(bid) if not full_id then return nil, err end local packs = get_op_packs(full_id) if not packs then return nil, "failed to read op packs for " .. full_id end if #packs == 0 then return nil, "no operation packs for bug " .. full_id end local title = "" local is_open = true local creator_id = "" local created_at = 0 local edited_at = 0 local labels = {} local comment_count = 0 for _, pack in ipairs(packs) do local author_id = "" local author_tbl = jval(pack.author, nil) if author_tbl then author_id = jval(author_tbl.id, "") end local ops = jval(pack.ops, {}) for _, op in ipairs(ops) do local op_type = jval(op.type, 0) local ts = jval(op.timestamp, 0) if ts > edited_at then edited_at = ts end if op_type == M.OP_CREATE then title = jval(op.title, "") creator_id = author_id created_at = ts if jval(op.message, "") ~= "" then comment_count = comment_count + 1 end elseif op_type == M.OP_SET_TITLE then title = jval(op.title, title) elseif op_type == M.OP_ADD_COMMENT then comment_count = comment_count + 1 elseif op_type == M.OP_SET_STATUS then local status_val = jval(op.status, M.STATUS_OPEN) is_open = (status_val == M.STATUS_OPEN) elseif op_type == M.OP_LABEL_CHANGE then for _, lbl in ipairs(jval(op.added, {})) do labels[lbl] = true end for _, lbl in ipairs(jval(op.removed, {})) do labels[lbl] = nil end end end end return { id = full_id, title = title, status = is_open and M.STATUS_OPEN or M.STATUS_CLOSED, creator_id = creator_id, created_at = created_at, edited_at = edited_at, labels = labels, comment_count = comment_count, } end -- ── Cache management ────────────────────────────────────────────────── function M.clear_cache() identity_cache = {} end return M python-ezgb-0.2.1/pyproject.toml000066400000000000000000000033311526117125300166770ustar00rootroot00000000000000[build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [project] name = 'ezgb' version = '0.2.1' description = 'A standalone Python library for working with git-bug repositories' readme = 'README.md' requires-python = '>=3.10' dependencies = ['pygit2'] license = 'GPL-2.0-or-later' license-files = ['COPYING'] authors = [ { name = 'Konstantin Ryabitsev', email = 'konstantin@linuxfoundation.org' } ] classifiers = [ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Developers', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development :: Bug Tracking', 'Topic :: Software Development :: Version Control :: Git', ] [dependency-groups] dev = [ "mypy", "pyright", "pytest", "ruff", "ty", "typing-extensions>=4.4", ] [project.urls] 'Homepage' = 'https://git.kernel.org/pub/scm/utils/ezgb/ezgb.git/' 'Source' = 'https://git.kernel.org/pub/scm/utils/ezgb/ezgb.git' 'Community' = 'https://lore.kernel.org/tools' [tool.setuptools.packages.find] where = ["src"] [tool.pytest.ini_options] testpaths = ["tests"] addopts = "--tb=short" [tool.mypy] strict = true [tool.ruff.lint] select = ["E", "F", "W", "I"] [tool.ruff.format] quote-style = "single" [tool.pyright] # pyright automatically ignores virtual environments and other directories, # but once we specify `exclude`, we're on our own. ".venv-*" catches the # per-interpreter venvs that ci-matrix.sh leaves behind (without them, # pyright tries to type-check every package in each one). exclude = [".venv", ".venv-*"] typeCheckingMode = "strict" executionEnvironments = [ { root = "tests", reportPrivateUsage = false }, ] [tool.ty.rules] all = "error" python-ezgb-0.2.1/requirements.txt000066400000000000000000000300301526117125300172430ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile pyproject.toml --generate-hashes -o requirements.txt cffi==2.0.0 \ --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf # via pygit2 pycparser==3.0 \ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi pygit2==1.19.2 \ --hash=sha256:02a35d56126f82a303668f4198c138627b3e9820f9f1eec38fff0409be274b9e \ --hash=sha256:033d489186145cf67b2c60840d2a308f6b1e9d641de12417c447f9829dacde70 \ --hash=sha256:06d2db3bdbf2906eb17112adb14a2fe6e34c1b2bce39c91819f59208d4e56665 \ --hash=sha256:0bd4059964531d20aaf4577b3761590df9cc7c9e2395df5d33f0552224331b76 \ --hash=sha256:0d9c795155086c95ef890c87b50e02792146cfaede2c715698e6988a122373e7 \ --hash=sha256:173165b54a2affed918302193f12dd369bec981b1d77904cdcd76b966a824e15 \ --hash=sha256:1a810da2d108d6bd16115c72a1c3d69fa1528ef927719bdfc94d2cdbc4198288 \ --hash=sha256:1cf08b54553f997f6f60a7918504e22e7baa4ba2fbb11d1e1cb6c0a45ac7e04b \ --hash=sha256:1ed09804dc6b6de0be07a71443122fd7b6458f8466d1134003c2dea55af886fc \ --hash=sha256:219c03bdbca59bd1df12b8bc7974b429872f4267aa2287ec0237c268593c0c5e \ --hash=sha256:222f439d751799dc74c3fa75f187abdbc415d12f9a091efa66f0c9ff51893d32 \ --hash=sha256:2805a8abd546e38298ce5daf33e444960e483acce68cbfb5d338e72ad5bc3503 \ --hash=sha256:2d114aa066e718d5ef3401b366dcb0b37b549c3b3b139f5f0042bd7059a4b0f7 \ --hash=sha256:330430b6c1a3e6d45d1f5f950734d37d849c07924b5b0475cd995a7e541e6ab1 \ --hash=sha256:376a0d2c27c082f6bd8b97fd8ffc1939f16dfe8374ec846deee9b11151b37b8a \ --hash=sha256:3cc461245b70be45a936e925744e67a45f6b0ee970aeb8e7a385dd7fe9f40877 \ --hash=sha256:4c2d397c887ff5a26b48ebd1bb9c66d2195ad377f0a44e05b79c462fff4040cd \ --hash=sha256:57d113a3eb61621ce16ceaa4bae7a93ffe525fd69da905445a0cf798d3601815 \ --hash=sha256:5bebea045102e87dea142242298d4dd668d0227f76042f98efb1c5d5dd3db21e \ --hash=sha256:5ec3538d81963bd05dd16c0de75938a9173966e1c853ad7848ebcb60bcfe21b0 \ --hash=sha256:60d011496e57436b0c8e3fbd4d12745777427b3f33a60710ec3d94d2f76304b7 \ --hash=sha256:6166845f41d4f6be3353997022d64035fe3df348c8e34d7d30c5f95817fbcab4 \ --hash=sha256:637d7c023f6623da35cf02cd1091f260c709730dd615367f4524ec8d771d0898 \ --hash=sha256:697e3684cb4ef2bfc084623c3f680d5ae8b4c8afca31a35a731b7b70204d9f83 \ --hash=sha256:69a0d377ee46110bbeea9e4191edee05132d1e7ac84b7cdebc640bc45868a2ec \ --hash=sha256:6b4de5469e88e7b069143f7a5d6336a4b3e7d911de4633ef18c113e416feb948 \ --hash=sha256:6e6e7eb5fb49203735627b8e1d410afe19e7d610c9a9733c11084fabd17f0920 \ --hash=sha256:70c7efc426bdae6b67465a03729b79277e7757a29a7d6550b40c18ed36cb7232 \ --hash=sha256:7659d59eba6c4a706978237d02e8d719f960843df749256f1656c938c1f4142b \ --hash=sha256:7b7f165d1ddfa1e0f205c1115ee10f5fea700fd3584c727b0d61a57192238449 \ --hash=sha256:7b96d6ed7251eef70cfd4126269f1044fa47bc6da6367300027c5e5d74789f7f \ --hash=sha256:7bbfeb680821001a5c1b6959da1eae906806c90c9992ae4564d3ea83a27bb19f \ --hash=sha256:837f0a9a0093cbb213176284d29f0ab754ded3e5af967e7ec6419d590a7da92a \ --hash=sha256:8a3643e4dd569c2909e88586659f617f70315680ca3c619cd8ff9e9c28726c25 \ --hash=sha256:8a7e99e5dfc8d3ed8f849b9688bc3fb1bdc86f34af28159140a8d1e18b703dd8 \ --hash=sha256:96f3235db6b553b8fb4d3c1dc86af9be1eab445f1d6c42f4ade5cf5f60efd333 \ --hash=sha256:9b0d5a44ca6d77a8c0e2526f6556d9b37cc85d44983ff3549bf5adbf95d289c4 \ --hash=sha256:9e0bc207abbef4d3be3bd37e0711e6974a148d41806fdc932aef9bb244b157c4 \ --hash=sha256:ae884cd53e29b3d831f5261f36048a8d5db5642dc98cd63530810e7fd9c9e60d \ --hash=sha256:c1becc06071acfdd5ae8523aaeab6d4b0930b2bcb08f5eb878e052e61275000b \ --hash=sha256:c3befcccc7b3b62e45da2cc1ce4095964f7606d3d15b43dc667c6ef2a2ada20d \ --hash=sha256:cb686bc81dfe5b13937047643fddb1dd253dae33b4a9ca62858c49ed294e05be \ --hash=sha256:cbeb3dbca9ca6ee3d5ea5d02f5e844c2d6084a2d5d6621e3e06aa2b11c645bfd \ --hash=sha256:cf479077d48a60b09569a5bb50866d8609f434f8982058594b0d2e2950bd6fce \ --hash=sha256:d02ebb50ea082d9631bbfda12787eb5324b8880a72cb8e3b9f11e9b323ad5781 \ --hash=sha256:d0b8ae5a822afb2771cbacf7c75140e663bc801c44eaaf2e4017f850cb27227c \ --hash=sha256:d0d2437bd5f8dbd652e8a6c318cbcaa245c0528ee48f6d64f4aaef8fd9b36b93 \ --hash=sha256:d7f630e5a763f01b4be6e2374c487086229c8f7392a2e5591d29095c5e481da4 \ --hash=sha256:df207f93a33851a110dec70108e3f2a1c69578932919fd356303eda83a5624db \ --hash=sha256:e46ec6a97a5c43704473e42a926f7f20f9934ceef4f4891660313f573c4f0ab8 \ --hash=sha256:e551908dfd93d471c0b08cfcddbe4924417865aae6ac90d20f3815c9483b0a82 \ --hash=sha256:e59a2e9eddd59edf999403c266c891dfc171eb95939d229ed614bc21e0c95804 \ --hash=sha256:eb1fd8538372230f8a471a5f3629901bc2fc7df992853d97bedc8fa269a9caf3 \ --hash=sha256:f064748202928f4e882501521229e378e0b7b69b0e7c433cdb2626d007745973 \ --hash=sha256:f5effee3f4ad0d9c89b34ebecf1acee26f6b117ef3c51345ad022bd521fd8dca \ --hash=sha256:ff32adce1a48d76b10e790b36784f6cb5ef40699b758c8b84f7f53f13b13d237 # via ezgb (pyproject.toml) python-ezgb-0.2.1/src/000077500000000000000000000000001526117125300145525ustar00rootroot00000000000000python-ezgb-0.2.1/src/ezgb/000077500000000000000000000000001526117125300155015ustar00rootroot00000000000000python-ezgb-0.2.1/src/ezgb/__init__.py000066400000000000000000000301731526117125300176160ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2024 by the Linux Foundation """ezgb: a standalone Python library for git-bug repositories.""" from __future__ import annotations from collections.abc import Iterator from datetime import datetime, timezone from ezgb._models import ( AmbiguousBugIdError, Bug, BugNotFoundError, BugSummary, CliError, Comment, EzgbError, GitError, Identity, IdentityNotFoundError, Status, UnsupportedFormatError, ) from ezgb._reader import BugReader, parse_since from ezgb._types import JsonValue, json_loads from ezgb._writer import BugWriter __all__ = [ 'GitBugRepo', 'Bug', 'BugSummary', 'Comment', 'Identity', 'Status', 'EzgbError', 'BugNotFoundError', 'AmbiguousBugIdError', 'IdentityNotFoundError', 'UnsupportedFormatError', 'GitError', 'CliError', ] class GitBugRepo: """High-level interface to a git-bug repository. Reads bug data directly from git objects for speed. Writes go through the ``git bug`` CLI to maintain Lamport clock and DAG consistency. Usage:: repo = GitBugRepo('/path/to/repo') for bug in repo.list_bugs(status=Status.OPEN): print(bug.title) """ def __init__(self, repo_path: str) -> None: self._repo = repo_path self._reader = BugReader(repo_path) self._writer = BugWriter(repo_path, self._reader) # -- Read operations ----------------------------------------------------- @staticmethod def _since_to_ts(since: str | datetime | int | None) -> int: """Normalise a *since* value to a unix timestamp (0 = no filter).""" if since is None: return 0 if isinstance(since, int): return since if isinstance(since, datetime): if since.tzinfo is None: since = since.replace(tzinfo=timezone.utc) return int(since.timestamp()) # str -- delegate to the reader's parser return parse_since(since) def list_bugs( self, *, status: Status | None = None, label: str | None = None, since: str | datetime | int | None = None, ) -> list[Bug]: """List all bugs, optionally filtered by status, label, and/or modification time. *since* accepts a unix timestamp (``int``), a :class:`~datetime.datetime`, or a string in ISO-8601 / ``YYYYMMDDHHMMSS`` format. Only bugs whose tip commit is newer than this value are returned. """ return list( self.iter_bugs( status=status, label=label, since=since, ) ) def iter_bugs( self, *, status: Status | None = None, label: str | None = None, since: str | datetime | int | None = None, ) -> Iterator[Bug]: """Lazily iterate over bugs, yielding one at a time. Accepts the same filters as :meth:`list_bugs` but avoids building all snapshots up front, which saves memory on large repositories. """ ts = self._since_to_ts(since) refs = self._reader.list_bug_refs(since=ts) for bid, _commit in refs: try: bug = self._reader.build_bug(bid) except BugNotFoundError: continue if status is not None and bug.status != status: continue if label is not None and label not in bug.labels: continue yield bug def list_bug_summaries( self, *, status: Status | None = None, label: str | None = None, since: str | datetime | int | None = None, ) -> list[BugSummary]: """List lightweight bug summaries, optionally filtered. Uses the git-bug CLI cache when available (~100ms for hundreds of bugs). Falls back to native git object reads when the CLI is not installed or the cache is empty. """ # Fast path: try the git-bug CLI cache if since is None: cached = self._list_summaries_from_cli() if cached is not None: results = cached if status is not None: results = [s for s in results if s.status == status] if label is not None: results = [s for s in results if label in s.labels] return results # Slow path: native git object reads return list( self.iter_bug_summaries( status=status, label=label, since=since, ) ) def _list_summaries_from_cli(self) -> list[BugSummary] | None: """Try to list summaries via the git-bug CLI cache. Returns None if the CLI is unavailable or returns an error, signalling the caller to fall back to native reads. """ import json import shutil if shutil.which('git-bug') is None: return None from ezgb._git import git_bug_cli ecode, out, _err = git_bug_cli(self._repo, ['bug', '-f', 'json']) if ecode != 0 or not out.strip(): return None try: raw_bugs: JsonValue = json_loads(out) except json.JSONDecodeError: return None assert isinstance(raw_bugs, list) results: list[BugSummary] = [] for raw in raw_bugs: assert isinstance(raw, dict) bid = str(raw.get('id', '')) if not bid: continue status_str = str(raw.get('status', 'open')) bug_status = Status.CLOSED if status_str == 'closed' else Status.OPEN create_time = raw.get('create_time') or {} edit_time = raw.get('edit_time') or {} ct = create_time.get('timestamp', 0) if isinstance(create_time, dict) else 0 if not isinstance(ct, int): ct = 0 et = edit_time.get('timestamp', 0) if isinstance(edit_time, dict) else 0 if not isinstance(et, int): et = 0 author = raw.get('author') or {} author_name = author.get('name', '') if isinstance(author, dict) else '' if not isinstance(author_name, str): author_name = '' author_id = author.get('id', '') if isinstance(author, dict) else '' raw_labels = raw.get('labels') or [] if isinstance(raw_labels, list): labels: frozenset[str] = frozenset(str(lb) for lb in raw_labels) else: labels = frozenset() comment_count = raw.get('comments', 0) if not isinstance(comment_count, int): comment_count = 0 results.append( BugSummary( id=bid, title=str(raw.get('title', '')), status=bug_status, creator_id=str(author_id), created_at=datetime.fromtimestamp(int(ct), tz=timezone.utc), labels=labels, comment_count=comment_count, author_name=author_name, edited_at=datetime.fromtimestamp(int(et), tz=timezone.utc), ) ) return results def iter_bug_summaries( self, *, status: Status | None = None, label: str | None = None, since: str | datetime | int | None = None, ) -> Iterator[BugSummary]: """Lazily iterate over bug summaries. Accepts the same filters as :meth:`list_bug_summaries`. """ ts = self._since_to_ts(since) refs = self._reader.list_bug_refs(since=ts) for bid, _commit in refs: try: summary = self._reader.build_bug_summary(bid) except BugNotFoundError: continue if status is not None and summary.status != status: continue if label is not None and label not in summary.labels: continue yield summary def get_bug(self, bid: str) -> Bug: """Get a single bug by ID (full or abbreviated).""" bid = self._reader.resolve_bug_id(bid) return self._reader.build_bug(bid) def get_attachment(self, blob_hash: str) -> bytes: """Read a file attachment by its blob hash. Blob hashes are found in :attr:`Comment.attachment_ids`. """ return self._reader.cat_blob_bytes(blob_hash) def search(self, query: str) -> list[Bug]: """Search bugs using the git-bug CLI query language. Delegates to ``git bug -f json `` and reconstructs Bug objects from the results. """ import json from ezgb._git import git_bug_cli ecode, out, _err = git_bug_cli(self._repo, ['bug', '-f', 'json', query]) if ecode != 0: return [] try: raw_bugs: JsonValue = json_loads(out) except json.JSONDecodeError: return [] assert isinstance(raw_bugs, list) results: list[Bug] = [] for raw in raw_bugs: assert isinstance(raw, dict) bid = str(raw.get('id', '')) if not bid: continue try: bug = self._reader.build_bug(bid) results.append(bug) except (BugNotFoundError, AmbiguousBugIdError): continue return results def list_identities(self) -> list[Identity]: """List all known identities.""" refs = self._reader.list_identity_refs() identities: list[Identity] = [] for iid, _commit in refs: identity = self._reader.resolve_identity(iid) identities.append(identity) return identities def resolve_bug_id(self, bid: str) -> str: """Resolve an abbreviated bug ID to the full 64-char ID.""" return self._reader.resolve_bug_id(bid) # -- Write operations ---------------------------------------------------- def create_bug(self, title: str, body: str) -> Bug: """Create a new bug and return its snapshot.""" return self._writer.create_bug(title, body) def add_comment(self, bid: str, text: str) -> Comment: """Add a comment to a bug and return the new Comment.""" return self._writer.add_comment(bid, text) def edit_comment(self, bid: str, comment_id: str, text: str) -> None: """Edit a comment's text.""" self._writer.edit_comment(bid, comment_id, text) def set_status(self, bid: str, status: Status) -> None: """Set a bug's status to open or closed.""" self._writer.set_status(bid, status) def set_title(self, bid: str, title: str) -> None: """Edit a bug's title.""" self._writer.set_title(bid, title) def add_label(self, bid: str, label: str) -> None: """Add a label to a bug.""" self._writer.add_label(bid, label) def remove_label(self, bid: str, label: str) -> None: """Remove a label from a bug.""" self._writer.remove_label(bid, label) def remove_bug(self, bid: str) -> None: """Permanently delete a bug. This is irreversible.""" self._writer.remove_bug(bid) def push(self, remote: str = 'origin') -> tuple[int, str, str]: """Push bugs and identities to a remote. Returns ``(returncode, stdout, stderr)``. """ return self._writer.push(remote) def pull(self, remote: str = 'origin') -> tuple[int, str, str]: """Pull bugs and identities from a remote and merge. Returns ``(returncode, stdout, stderr)``. Caches are invalidated automatically after pull. """ result = self._writer.pull(remote) self._reader.invalidate() return result # -- Cache management ---------------------------------------------------- def invalidate(self, bid: str | None = None) -> None: """Clear cached data. If *bid* is given, only that bug is evicted. Otherwise all caches are cleared. """ self._reader.invalidate(bid) python-ezgb-0.2.1/src/ezgb/_git.py000066400000000000000000000041331526117125300167760ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2024 by the Linux Foundation """Thin git subprocess helpers for ezgb.""" from __future__ import annotations import os import subprocess from typing import Union def git_run( repo_path: str, args: list[str], stdin: bytes | None = None, decode: bool = True, ) -> tuple[int, Union[str, bytes]]: """Run a git command against a repository. Uses ``--git-dir`` so it works on both bare and non-bare repos. Automatically appends ``--no-abbrev-commit`` for ``log`` commands. """ cmdargs = ['git', '--no-pager'] gitdir = repo_path if os.path.isdir(os.path.join(repo_path, '.git')): gitdir = os.path.join(repo_path, '.git') cmdargs += ['--git-dir', gitdir] args = list(args) if args and args[0] == 'log': args.insert(1, '--no-abbrev-commit') cmdargs += args sp = subprocess.run(cmdargs, input=stdin, capture_output=True) if decode: return sp.returncode, sp.stdout.decode(errors='replace') return sp.returncode, sp.stdout def git_lines(repo_path: str, args: list[str]) -> list[str]: """Run a git command and return non-empty output lines.""" _ecode, out = git_run(repo_path, args) if not isinstance(out, str): out = out.decode(errors='replace') return [line for line in out.split('\n') if line] def git_bug_cli( repo_path: str, args: list[str], stdin: str | None = None, ) -> tuple[int, str, str]: """Run ``git -C REPO bug `` for write operations. Uses ``-C`` (not ``--git-dir``) because the git-bug CLI needs a working tree context. When *stdin* is provided it is piped to the process on stdin. Returns ``(returncode, stdout, stderr)``. """ cmdargs = ['git', '--no-pager', '-C', repo_path, 'bug'] + args in_bytes = stdin.encode() if stdin is not None else None sp = subprocess.run(cmdargs, input=in_bytes, capture_output=True) return ( sp.returncode, sp.stdout.decode(errors='replace'), sp.stderr.decode(errors='replace'), ) python-ezgb-0.2.1/src/ezgb/_models.py000066400000000000000000000063231526117125300175010ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2024 by the Linux Foundation """Data models, enums, exceptions, and constants for ezgb.""" from __future__ import annotations import enum from dataclasses import dataclass, field from datetime import datetime, timezone _EPOCH_UTC = datetime.min.replace(tzinfo=timezone.utc) # -- Exceptions -------------------------------------------------------------- class EzgbError(Exception): """Base exception for all ezgb errors.""" class BugNotFoundError(EzgbError): """A bug ID could not be resolved or has no operation packs.""" class AmbiguousBugIdError(EzgbError): """A bug ID prefix matches multiple bugs.""" class IdentityNotFoundError(EzgbError): """An identity ID could not be resolved.""" class UnsupportedFormatError(EzgbError): """Git-bug objects use an unsupported format version.""" class GitError(EzgbError): """A git subprocess failed unexpectedly.""" class CliError(EzgbError): """A git-bug CLI write operation failed.""" # -- Enums ------------------------------------------------------------------- class Status(enum.Enum): """Bug status matching git-bug's common.Status values.""" OPEN = 1 CLOSED = 2 # -- Constants --------------------------------------------------------------- # git-bug format versions we support SUPPORTED_BUG_FORMAT: int = 4 SUPPORTED_IDENTITY_FORMAT: int = 2 # Operation type constants (from entities/bug/operation.go) OP_CREATE: int = 1 OP_SET_TITLE: int = 2 OP_ADD_COMMENT: int = 3 OP_SET_STATUS: int = 4 OP_LABEL_CHANGE: int = 5 OP_EDIT_COMMENT: int = 6 OP_NOOP: int = 7 OP_SET_METADATA: int = 8 # Status values used in operation JSON STATUS_OPEN: int = 1 STATUS_CLOSED: int = 2 # -- Dataclasses ------------------------------------------------------------- @dataclass(frozen=True) class Identity: """A git-bug user identity.""" id: str name: str email: str login: str = '' @dataclass class Comment: """A comment on a bug.""" id: str author: Identity text: str created_at: datetime count: int attachment_ids: list[str] = field(default_factory=list[str]) @dataclass class Bug: """A git-bug bug snapshot reconstructed from operations.""" id: str title: str status: Status creator: Identity created_at: datetime labels: set[str] comments: list[Comment] metadata: dict[str, str] = field(default_factory=dict[str, str]) @dataclass(frozen=True) class BugSummary: """Lightweight bug snapshot for list views. Contains only the fields needed for display in listings. Identity resolution is deferred -- *creator_id* holds the raw identity hash string rather than a resolved Identity object. Both the git-bug CLI cache and native git-object reads populate *author_name* (the creator's resolved display name) and *edited_at* (the last-activity time). *author_name* may fall back to the raw identity hash if the identity cannot be resolved. """ id: str title: str status: Status creator_id: str created_at: datetime labels: frozenset[str] comment_count: int author_name: str = '' edited_at: datetime = _EPOCH_UTC python-ezgb-0.2.1/src/ezgb/_reader.py000066400000000000000000000665211526117125300174660ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2024 by the Linux Foundation """Git object reading, operation pack replay, and caching for ezgb.""" from __future__ import annotations import hashlib import json import logging import os from datetime import datetime, timezone from typing import TypeGuard, TypeVar import pygit2 from ezgb._models import ( OP_ADD_COMMENT, OP_CREATE, OP_EDIT_COMMENT, OP_LABEL_CHANGE, OP_NOOP, OP_SET_METADATA, OP_SET_STATUS, OP_SET_TITLE, STATUS_OPEN, SUPPORTED_BUG_FORMAT, SUPPORTED_IDENTITY_FORMAT, AmbiguousBugIdError, Bug, BugNotFoundError, BugSummary, Comment, Identity, Status, UnsupportedFormatError, ) from ezgb._types import JsonObject, JsonValue, json_loads, json_raw_decode logger = logging.getLogger('ezgb') # Length of a full git-bug entity ID (SHA-256 hex) _ID_LENGTH = 64 _K = TypeVar('_K') _T = TypeVar('_T') _U = TypeVar('_U') def _is_list(value: list[_T], item_ty: type[_U]) -> TypeGuard[list[_U]]: return all(isinstance(item, item_ty) for item in value) def _is_dict_values(value: dict[_K, _T], value_ty: type[_U]) -> TypeGuard[dict[_K, _U]]: return all(isinstance(value, value_ty) for value in value.values()) def parse_since(since: str) -> int: """Parse a datetime string to a unix timestamp. Accepts ISO-8601 variants and ``YYYYMMDDHHMMSS``. """ since = since.strip() for fmt in ( '%Y-%m-%d %H:%M:%S', '%Y%m%d%H%M%S', '%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S', ): try: dt = datetime.strptime(since, fmt).replace(tzinfo=timezone.utc) return int(dt.timestamp()) except ValueError: continue raise ValueError('cannot parse since=%s' % since) def _combine_ids(primary: str, secondary: str) -> str: """Interleave primary and secondary IDs into a CombinedId. Mirrors git-bug's CombineIds function. The interleaving pattern places secondary characters at positions 1, 3, 5, 9, and then every position where ``i >= 10 and i % 5 == 4``. Format: PSPSPSPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPP (P=primary, S=secondary) """ result: list[str] = [] pi = 0 si = 0 for i in range(_ID_LENGTH): if i in (1, 3, 5, 9) or (i >= 10 and i % 5 == 4): result.append(secondary[si]) si += 1 else: result.append(primary[pi]) pi += 1 return ''.join(result) class BugReader: """Reads bug and identity data from git-bug's git object storage.""" def __init__(self, repo_path: str) -> None: self._repo = repo_path gitdir = repo_path if os.path.isdir(os.path.join(repo_path, '.git')): gitdir = os.path.join(repo_path, '.git') self._pygit: pygit2.Repository = pygit2.Repository(gitdir) self._bug_cache: dict[str, Bug] = {} self._summary_cache: dict[str, BugSummary] = {} self._identity_cache: dict[str, Identity] = {} self._resolve_cache: dict[str, str] = {} # -- Blob reading (pygit2) ------------------------------------------------- def _cat_blob(self, blob_hash: str) -> str: """Read a single git blob as text.""" try: obj = self._pygit.get(blob_hash) except (ValueError, KeyError): obj = None if obj is None or not isinstance(obj, pygit2.Blob): raise BugNotFoundError('failed to read blob %s' % blob_hash) result: str = obj.data.decode(errors='replace') return result def cat_blob_bytes(self, blob_hash: str) -> bytes: """Read a single git blob as raw bytes.""" try: obj = self._pygit.get(blob_hash) except (ValueError, KeyError): obj = None if obj is None or not isinstance(obj, pygit2.Blob): raise BugNotFoundError('failed to read blob %s' % blob_hash) return bytes(obj.data) def _walk_ref_tree_blobs( self, refname: str, target_file: str = 'ops', ) -> list[tuple[str, str]]: """Walk the commit chain for a ref and find named blobs. Returns ``[(commit_hex, blob_hex)]`` oldest first, for each commit whose tree contains *target_file*. """ try: ref = self._pygit.references.get(refname) except (ValueError, KeyError): ref = None if ref is None: return [] tip = ref.peel(pygit2.Commit) entries: list[tuple[str, str]] = [] commit: pygit2.Commit | None = tip while commit is not None: tree = commit.tree if target_file in tree: blob_entry = tree[target_file] entries.append((str(commit.id), str(blob_entry.id))) commit = commit.parents[0] if commit.parents else None entries.reverse() return entries # -- Format version checking --------------------------------------------- @staticmethod def _check_format_version_tree( tree: pygit2.Tree, prefix: str, supported: int, ) -> None: """Validate the git-bug format version from a commit tree. Scans tree entries for names matching ``{prefix}{N}`` and raises UnsupportedFormatError if the version does not match. """ for entry in tree: name: str = entry.name or '' if name.startswith(prefix): try: version = int(name[len(prefix) :]) except ValueError: continue if version != supported: raise UnsupportedFormatError( 'unsupported git-bug format: %s%d (expected %s%d)' % (prefix, version, prefix, supported) ) return # -- Ref enumeration ----------------------------------------------------- def _list_refs( self, prefix: str, *, since: int = 0, ) -> list[tuple[str, str]]: """Return ``[(entity_id, commit_hex)]`` for refs under *prefix*. If *since* is positive, only refs whose tip commit is newer than that unix timestamp are returned. """ results: list[tuple[str, str]] = [] for refname in self._pygit.references: if not refname.startswith(prefix): continue ref = self._pygit.references.get(refname) if ref is None: continue commit = ref.peel(pygit2.Commit) if since > 0 and commit.commit_time < since: continue eid = refname.rsplit('/', 1)[-1] results.append((eid, str(commit.id))) return results def list_bug_refs( self, *, since: int = 0, ) -> list[tuple[str, str]]: """Return ``[(bug_id, commit_hash)]`` for all bugs.""" return self._list_refs('refs/bugs/', since=since) def list_identity_refs(self) -> list[tuple[str, str]]: """Return ``[(identity_id, commit_hash)]`` for all identities.""" return self._list_refs('refs/identities/') # -- Bug ID resolution --------------------------------------------------- def resolve_bug_id(self, bid: str) -> str: """Resolve a possibly-abbreviated bug ID to the full ref name. Raises AmbiguousBugIdError when more than one ref matches. Raises BugNotFoundError when no ref matches. """ if bid in self._resolve_cache: return self._resolve_cache[bid] prefix = 'refs/bugs/' + bid candidates: list[str] = [ str(refname).rsplit('/', 1)[-1] for refname in self._pygit.references if str(refname).startswith(prefix) ] if len(candidates) == 1: full_bid = candidates[0] self._resolve_cache[bid] = full_bid self._resolve_cache[full_bid] = full_bid return full_bid if len(candidates) > 1: raise AmbiguousBugIdError( 'bug ID prefix %s matches %d bugs' % (bid, len(candidates)) ) raise BugNotFoundError('no bug matching ID prefix %s' % bid) # -- Operation pack parsing ---------------------------------------------- def _get_op_packs( self, bid: str, ) -> tuple[list[JsonObject], list[str]]: """Walk the commit chain for a bug and return operation packs (oldest first) together with the raw blob strings. Returns ``(packs, raw_blobs)`` where each element in *raw_blobs* is the original JSON text of the corresponding pack. The raw strings are needed for correct operation hashing. """ bid = self.resolve_bug_id(bid) refname = 'refs/bugs/' + bid entries = self._walk_ref_tree_blobs(refname, 'ops') if not entries: return [], [] # Check format version from the first commit's tree ref = self._pygit.references.get(refname) if ref is not None: tip = ref.peel(pygit2.Commit) self._check_format_version_tree(tip.tree, 'version-', SUPPORTED_BUG_FORMAT) packs: list[JsonObject] = [] raw_blobs: list[str] = [] for _commit, blob_hash in entries: raw = self._cat_blob(blob_hash) try: pack: JsonValue = json_loads(raw) except json.JSONDecodeError: logger.warning( 'failed to parse ops blob %s for bug %s', blob_hash, bid, ) continue assert isinstance(pack, dict) packs.append(pack) raw_blobs.append(raw) return packs, raw_blobs # -- Identity resolution ------------------------------------------------- def resolve_identity(self, identity_id: str) -> Identity: """Resolve an identity ID to an Identity dataclass. Reads the most recent version blob from the identity's commit chain. Identity versions use a flat JSON format (not op-packs). """ if identity_id in self._identity_cache: return self._identity_cache[identity_id] fallback = Identity( id=identity_id, name=identity_id, email=identity_id, ) refname = 'refs/identities/' + identity_id try: ref = self._pygit.references.get(refname) except (ValueError, KeyError): ref = None if ref is None: self._identity_cache[identity_id] = fallback return fallback # Get the tip commit's tree and look for 'version' blob tip = ref.peel(pygit2.Commit) if 'version' not in tip.tree: self._identity_cache[identity_id] = fallback return fallback version_blob = tip.tree['version'] try: blob = self._pygit[version_blob.id] assert isinstance(blob, pygit2.Blob) raw = blob.data.decode(errors='replace') data: JsonValue = json_loads(raw) except (json.JSONDecodeError, KeyError): self._identity_cache[identity_id] = fallback return fallback assert isinstance(data, dict) # Validate format version fmt_version = data.get('version') if fmt_version is not None: assert isinstance(fmt_version, int) if fmt_version is not None and fmt_version != SUPPORTED_IDENTITY_FORMAT: raise UnsupportedFormatError( 'unsupported identity format: %d (expected %d)' % (fmt_version, SUPPORTED_IDENTITY_FORMAT) ) name = data.get('name', identity_id) email = data.get('email', identity_id) login = data.get('login', '') assert isinstance(name, str) assert isinstance(email, str) assert isinstance(login, str) identity = Identity( id=identity_id, name=name, email=email, login=login, ) self._identity_cache[identity_id] = identity return identity # -- Bug snapshot building ----------------------------------------------- @staticmethod def _op_hash(raw_op_json: str) -> str: """Compute a git-bug compatible operation ID. git-bug derives IDs as SHA256 of the compact JSON serialization. The raw JSON string from the blob must be used (not a re-serialized version) because Go's ``json.Marshal`` escapes characters like ``<``, ``>``, ``&`` as ``\\u003c`` etc., and Python's ``json.dumps`` does not reproduce that encoding. """ return hashlib.sha256(raw_op_json.encode()).hexdigest() @staticmethod def _extract_raw_ops(blob_json: str) -> list[str]: """Extract individual operation JSON strings from an ops blob. git-bug identifies each operation by hashing its JSON serialization with SHA-256. The serialization is produced by Go's ``json.Marshal``, which escapes ``<``, ``>``, and ``&`` as ``\\u003c``, ``\\u003e``, ``\\u0026`` — a Go-specific behaviour that Python's ``json.dumps`` does not reproduce. Re-serializing in Python therefore produces different bytes and a different hash, which silently breaks ``OP_EDIT_COMMENT`` matching (the ``target`` field carries the Go-computed hash). To get correct hashes we must use the exact bytes that git-bug wrote. This method locates each op object inside the original blob string using :meth:`json.JSONDecoder.raw_decode` and slices the string at those boundaries, preserving Go's escaping verbatim. """ try: pack, _ = json_raw_decode(blob_json) except json.JSONDecodeError: return [] assert isinstance(pack, dict) raw_ops = pack.get('ops') if not raw_ops: return [] assert isinstance(raw_ops, list) # Walk forward through the string to slice each op. # Find the start of the ops array value. idx = blob_json.find('"ops"') pos = blob_json.find('[', idx) + 1 ops: list[str] = [] for _ in raw_ops: # raw_decode skips leading whitespace on its own, but # we need to step past commas between elements. while blob_json[pos] in ' \t\n\r,': pos += 1 _decoded_op, end_pos = json_raw_decode(blob_json, pos) ops.append(blob_json[pos:end_pos]) pos = end_pos return ops @staticmethod def _format_timestamp(unix_ts: int) -> datetime: """Convert a unix timestamp to a timezone-aware datetime.""" return datetime.fromtimestamp(unix_ts, tz=timezone.utc) def build_bug( self, bid: str, packs: list[JsonObject] | None = None, raw_blobs: list[str] | None = None, ) -> Bug: """Reconstruct a bug snapshot by replaying operation packs. Returns a Bug dataclass. Results are cached by full bug ID. """ bid = self.resolve_bug_id(bid) if bid in self._bug_cache: return self._bug_cache[bid] if packs is None: packs, raw_blobs = self._get_op_packs(bid) if not packs: raise BugNotFoundError('no operation packs for bug %s' % bid) if raw_blobs is None: raw_blobs = [] # Extract raw op JSON strings from each blob for hashing raw_ops_per_pack: list[list[str]] = [ self._extract_raw_ops(blob) for blob in raw_blobs ] title = '' is_open = True creator: Identity | None = None created_at: datetime | None = None labels: set[str] = set() comments: list[Comment] = [] comment_count = 0 metadata: dict[str, str] = {} # Map operation hashes to Comment objects for OP_EDIT_COMMENT op_hash_map: dict[str, Comment] = {} for pack_idx, pack in enumerate(packs): author_value = pack.get('author', {}) assert isinstance(author_value, dict) author_id = author_value.get('id', '') assert isinstance(author_id, str) raw_ops = ( raw_ops_per_pack[pack_idx] if pack_idx < len(raw_ops_per_pack) else [] ) ops = pack.get('ops', []) assert isinstance(ops, list) for op_idx, op in enumerate(ops): assert isinstance(op, dict) op_type = op.get('type', 0) assert isinstance(op_type, int) timestamp = op.get('timestamp', 0) assert isinstance(timestamp, int) raw_json = raw_ops[op_idx] if op_idx < len(raw_ops) else '' op_id = self._op_hash(raw_json) if raw_json else '' if op_type == OP_CREATE: if (op_title := op.get('title')) is not None: assert isinstance(op_title, str) title = op_title author = self.resolve_identity(author_id) creator = author created_at = self._format_timestamp(timestamp) if message := op.get('message'): assert isinstance(message, str) files = op.get('files') or [] assert isinstance(files, list) assert _is_list(files, str) combined_id = _combine_ids(bid, op_id) cmt = Comment( id=combined_id, author=author, text=message, created_at=self._format_timestamp(timestamp), count=comment_count, attachment_ids=list(files), ) comments.append(cmt) op_hash_map[op_id] = cmt comment_count += 1 # Collect operation-level metadata op_meta = op.get('metadata') if op_meta is not None: assert isinstance(op_meta, dict) assert _is_dict_values(op_meta, str) metadata.update(op_meta) elif op_type == OP_SET_TITLE: if (op_title := op.get('title')) is not None: assert isinstance(op_title, str) title = op_title elif op_type == OP_ADD_COMMENT: message = op.get('message', '') assert isinstance(message, str) files = op.get('files') or [] assert isinstance(files, list) assert _is_list(files, str) op_author = self.resolve_identity(author_id) combined_id = _combine_ids(bid, op_id) cmt = Comment( id=combined_id, author=op_author, text=message, created_at=self._format_timestamp(timestamp), count=comment_count, attachment_ids=list(files), ) comments.append(cmt) op_hash_map[op_id] = cmt comment_count += 1 elif op_type == OP_SET_STATUS: status_val = op.get('status', STATUS_OPEN) assert isinstance(status_val, int) is_open = status_val == STATUS_OPEN elif op_type == OP_LABEL_CHANGE: added = op.get('added') or [] assert isinstance(added, list) for lbl in added: assert isinstance(lbl, str) labels.add(lbl) removed = op.get('removed') or [] assert isinstance(removed, list) for lbl in removed: assert isinstance(lbl, str) labels.discard(lbl) elif op_type == OP_EDIT_COMMENT: target = op.get('target', '') assert isinstance(target, str) new_message = op.get('message', '') assert isinstance(new_message, str) matched = op_hash_map.get(target) if matched is not None: matched.text = new_message new_files = op.get('files') or [] assert isinstance(new_files, list) assert _is_list(new_files, str) if new_files: matched.attachment_ids = list(new_files) elif op_type == OP_SET_METADATA: new_meta = op.get('new_metadata') if new_meta is not None: assert isinstance(new_meta, dict) assert _is_dict_values(new_meta, str) metadata.update(new_meta) elif op_type == OP_NOOP: pass # No-op, used for bridge metadata # Use a fallback identity if somehow no OP_CREATE was found if creator is None: creator = Identity(id='unknown', name='unknown', email='unknown') if created_at is None: created_at = datetime.fromtimestamp(0, tz=timezone.utc) bug = Bug( id=bid, title=title, status=Status.OPEN if is_open else Status.CLOSED, creator=creator, created_at=created_at, labels=labels, comments=comments, metadata=metadata, ) self._bug_cache[bid] = bug return bug def build_bug_summary( self, bid: str, packs: list[JsonObject] | None = None, ) -> BugSummary: """Build a lightweight bug summary by replaying operation packs. Unlike :meth:`build_bug`, this skips op hashing, comment text, EditComment replay, metadata, and per-op identity resolution. Only the raw *creator_id* string is stored; batch-resolve identities separately after collecting summaries. """ bid = self.resolve_bug_id(bid) if bid in self._summary_cache: return self._summary_cache[bid] if packs is None: packs, _raw_blobs = self._get_op_packs(bid) if not packs: raise BugNotFoundError('no operation packs for bug %s' % bid) title = '' is_open = True creator_id = '' created_at: datetime | None = None labels: set[str] = set() comment_count = 0 latest_ts = 0 for pack in packs: author_value = pack.get('author', {}) assert isinstance(author_value, dict) author_id = author_value.get('id', '') assert isinstance(author_id, str) ops = pack.get('ops', []) assert isinstance(ops, list) for op in ops: assert isinstance(op, dict) op_type = op.get('type', 0) assert isinstance(op_type, int) ts = op.get('timestamp', 0) if isinstance(ts, int) and ts > latest_ts: latest_ts = ts if op_type == OP_CREATE: if (op_title := op.get('title')) is not None: assert isinstance(op_title, str) title = op_title creator_id = author_id timestamp = op.get('timestamp', 0) assert isinstance(timestamp, int) created_at = self._format_timestamp(timestamp) if message := op.get('message'): assert isinstance(message, str) comment_count += 1 elif op_type == OP_SET_TITLE: if (op_title := op.get('title')) is not None: assert isinstance(op_title, str) title = op_title elif op_type == OP_ADD_COMMENT: comment_count += 1 elif op_type == OP_SET_STATUS: status_val = op.get('status', STATUS_OPEN) assert isinstance(status_val, int) is_open = status_val == STATUS_OPEN elif op_type == OP_LABEL_CHANGE: added = op.get('added') or [] assert isinstance(added, list) for lbl in added: assert isinstance(lbl, str) labels.add(lbl) removed = op.get('removed') or [] assert isinstance(removed, list) for lbl in removed: assert isinstance(lbl, str) labels.discard(lbl) if created_at is None: created_at = datetime.fromtimestamp(0, tz=timezone.utc) # Resolve the creator's display name and derive the last-activity # time so native summaries carry the same fields as those built # from the git-bug CLI cache. Without this, list views that read # author_name/edited_at (e.g. right after a pull invalidates the # CLI cache) show blank submitters and mis-sort by an epoch date. # resolve_identity is cached and cheap with pygit2, and only the # creator (one per bug) is resolved -- per-op identity resolution # stays deferred to build_bug(). author_name = '' if creator_id: author_name = self.resolve_identity(creator_id).name edited_at = self._format_timestamp(latest_ts) if latest_ts else created_at summary = BugSummary( id=bid, title=title, status=Status.OPEN if is_open else Status.CLOSED, creator_id=creator_id, created_at=created_at, labels=frozenset(labels), comment_count=comment_count, author_name=author_name, edited_at=edited_at, ) self._summary_cache[bid] = summary return summary def prefetch_bugs(self, bids: list[str]) -> None: """Warm the cache by building bugs for the given IDs. With pygit2, blob reads are already fast in-process calls, so this simply builds each bug and caches the result. """ for bid in bids: if bid in self._bug_cache: continue try: self.build_bug(bid) except BugNotFoundError: continue # -- Cache management ---------------------------------------------------- def invalidate(self, bid: str | None = None) -> None: """Clear cached data. If *bid* is given, only that bug is evicted. Otherwise all caches (bugs, identities, resolve) are cleared. """ if bid: try: full_bid = self.resolve_bug_id(bid) except (BugNotFoundError, AmbiguousBugIdError): full_bid = bid self._bug_cache.pop(full_bid, None) self._summary_cache.pop(full_bid, None) else: self._bug_cache.clear() self._summary_cache.clear() self._identity_cache.clear() self._resolve_cache.clear() python-ezgb-0.2.1/src/ezgb/_types.py000066400000000000000000000016061526117125300173610ustar00rootroot00000000000000"""Internal type aliases.""" from __future__ import annotations import json from typing import Union, cast JsonObject = dict[str, 'JsonValue'] JsonValue = Union[ None, bool, int, float, str, list['JsonValue'], JsonObject, ] # The json module only ever produces values of these types (when used # without custom hooks), but typeshed annotates its results as ``Any``. # These wrappers state that once, so callers get a precise type. _DECODER = json.JSONDecoder() def json_loads(s: str | bytes) -> JsonValue: """Parse a JSON document, like :func:`json.loads`.""" return cast(JsonValue, json.loads(s)) def json_raw_decode(s: str, pos: int = 0) -> tuple[JsonValue, int]: """Parse one JSON value at *pos*, like :meth:`json.JSONDecoder.raw_decode`. Returns ``(value, end_pos)``. """ return cast(tuple[JsonValue, int], _DECODER.raw_decode(s, pos)) python-ezgb-0.2.1/src/ezgb/_writer.py000066400000000000000000000137051526117125300175340ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2024 by the Linux Foundation """CLI wrappers for git-bug write operations.""" from __future__ import annotations import logging import re from ezgb._git import git_bug_cli from ezgb._models import ( Bug, CliError, Comment, Status, ) from ezgb._reader import BugReader logger = logging.getLogger('ezgb') # Matches "abc1234 created\n" from git bug new _NEW_BUG_RE = re.compile(r'^([0-9a-f]{7,})\s+created') class BugWriter: """Wraps the ``git bug`` CLI for write operations. All mutations go through the CLI to maintain Lamport clock and DAG consistency. The reader's cache is invalidated after each write so subsequent reads reflect the change. """ def __init__(self, repo_path: str, reader: BugReader) -> None: self._repo = repo_path self._reader = reader def _cli(self, args: list[str], stdin: str | None = None) -> tuple[int, str, str]: """Run a git-bug CLI command.""" return git_bug_cli(self._repo, args, stdin=stdin) def create_bug(self, title: str, body: str) -> Bug: """Create a new bug and return its snapshot. Parses the human_id from ``git bug new`` output, then resolves and builds the full Bug. """ # Use -F - to pipe the message via stdin, avoiding OS argument # size limits for large bodies. The format is the same as git # commit: first line = title, blank line, rest = body. message = f'{title}\n\n{body}' if body else title args = ['bug', 'new', '-F', '-', '--non-interactive'] ecode, out, err = self._cli(args, stdin=message) if ecode != 0: raise CliError('git bug new failed: %s' % err) # Parse human_id from output like "abc1234 created" match = _NEW_BUG_RE.match(out.strip()) if not match: raise CliError('could not parse bug ID from git bug new output: %s' % out) human_id = match.group(1) self._reader.invalidate() full_bid = self._reader.resolve_bug_id(human_id) return self._reader.build_bug(full_bid) def add_comment(self, bid: str, text: str) -> Comment: """Add a comment to a bug and return the new Comment.""" bid = self._reader.resolve_bug_id(bid) args = [ 'bug', 'comment', 'new', bid, '-F', '-', '--non-interactive', ] ecode, _out, err = self._cli(args, stdin=text) if ecode != 0: raise CliError('git bug comment new failed: %s' % err) self._reader.invalidate(bid) bug = self._reader.build_bug(bid) return bug.comments[-1] def edit_comment(self, bid: str, comment_id: str, text: str) -> None: """Edit a comment's text. *bid* is the bug ID (for cache invalidation). *comment_id* is the combined comment ID from ``Comment.id``. """ bid = self._reader.resolve_bug_id(bid) args = [ 'bug', 'comment', 'edit', comment_id, '-F', '-', '--non-interactive', ] ecode, _out, err = self._cli(args, stdin=text) if ecode != 0: raise CliError('git bug comment edit failed: %s' % err) self._reader.invalidate(bid) def set_status(self, bid: str, status: Status) -> None: """Set a bug's status to open or closed.""" bid = self._reader.resolve_bug_id(bid) if status == Status.CLOSED: subcmd = 'close' else: subcmd = 'open' ecode, _out, err = self._cli(['bug', 'status', subcmd, bid]) if ecode != 0: raise CliError('git bug status %s failed: %s' % (subcmd, err)) self._reader.invalidate(bid) def set_title(self, bid: str, title: str) -> None: """Edit a bug's title.""" bid = self._reader.resolve_bug_id(bid) args = ['bug', 'title', 'edit', bid, '-t', title] ecode, _out, err = self._cli(args) if ecode != 0: raise CliError('git bug title edit failed: %s' % err) self._reader.invalidate(bid) def add_label(self, bid: str, label: str) -> None: """Add a label to a bug.""" bid = self._reader.resolve_bug_id(bid) ecode, _out, err = self._cli( ['bug', 'label', 'new', bid, label], ) if ecode != 0: raise CliError('git bug label new failed: %s' % err) self._reader.invalidate(bid) def remove_label(self, bid: str, label: str) -> None: """Remove a label from a bug.""" bid = self._reader.resolve_bug_id(bid) ecode, _out, err = self._cli( ['bug', 'label', 'rm', bid, label], ) if ecode != 0: raise CliError('git bug label rm failed: %s' % err) self._reader.invalidate(bid) def remove_bug(self, bid: str) -> None: """Permanently delete a bug. Calls ``git bug rm`` which removes the local ref and all remote copies. This is irreversible. """ bid = self._reader.resolve_bug_id(bid) ecode, _out, err = self._cli(['bug', 'rm', bid]) if ecode != 0: raise CliError('git bug rm failed: %s' % err) # Invalidate everything — the refs list has changed self._reader.invalidate() def push(self, remote: str = 'origin') -> tuple[int, str, str]: """Push bugs and identities to a remote. Returns ``(returncode, stdout, stderr)`` so the caller can display output to the user. """ return self._cli(['push', remote]) def pull(self, remote: str = 'origin') -> tuple[int, str, str]: """Pull bugs and identities from a remote and merge. Returns ``(returncode, stdout, stderr)``. The caller should invalidate caches after a successful pull. """ return self._cli(['pull', remote]) python-ezgb-0.2.1/src/ezgb/py.typed000066400000000000000000000000001526117125300171660ustar00rootroot00000000000000python-ezgb-0.2.1/tests/000077500000000000000000000000001526117125300151255ustar00rootroot00000000000000python-ezgb-0.2.1/tests/conftest.py000066400000000000000000000201731526117125300173270ustar00rootroot00000000000000"""Shared fixtures and test data factories for ezgb tests. Creates real git objects in temporary bare repos using pygit2 so the BugReader can read them without subprocess mocking. """ from __future__ import annotations import json from pathlib import Path import pygit2 import pytest from typing_extensions import override from ezgb._reader import BugReader from ezgb._types import JsonObject from ezgb._writer import BugWriter # -- Constants --------------------------------------------------------------- IDENTITY_ID = 'a' * 64 IDENTITY_ID_2 = 'b' * 64 BUG_ID = 'c' * 64 # Signature used for all test commits _SIG = pygit2.Signature('Test', 'test@test.com') CliResult = tuple[int, str, str] # -- Test data factories ----------------------------------------------------- def make_identity_version(name: str, email: str) -> str: """Build a JSON identity version blob (format 2).""" return json.dumps( { 'version': 2, 'unix_time': 1700000000, 'name': name, 'email': email, 'login': email.split('@')[0], } ) def make_op_pack(author_id: str, ops: list[JsonObject]) -> str: """Build a JSON operation pack with author and ops array.""" return json.dumps( { 'author': {'id': author_id}, 'ops': ops, } ) def make_create_op(title: str, message: str, timestamp: int = 1700000000) -> JsonObject: """Build an OP_CREATE (type 1) operation.""" return { 'type': 1, 'timestamp': timestamp, 'title': title, 'message': message, 'files': [], } def make_comment_op(message: str, timestamp: int = 1700001000) -> JsonObject: """Build an OP_ADD_COMMENT (type 3) operation.""" return { 'type': 3, 'timestamp': timestamp, 'message': message, 'files': [], } def make_set_title_op(title: str, timestamp: int = 1700002000) -> JsonObject: """Build an OP_SET_TITLE (type 2) operation.""" return {'type': 2, 'timestamp': timestamp, 'title': title} def make_set_status_op(status: int, timestamp: int = 1700003000) -> JsonObject: """Build an OP_SET_STATUS (type 4) operation.""" return {'type': 4, 'timestamp': timestamp, 'status': status} def make_edit_comment_op( target: str, message: str, timestamp: int = 1700002500 ) -> JsonObject: """Build an OP_EDIT_COMMENT (type 6) operation.""" return { 'type': 6, 'timestamp': timestamp, 'target': target, 'message': message, } def make_label_change_op( added: list[str] | None = None, removed: list[str] | None = None, timestamp: int = 1700004000, ) -> JsonObject: """Build an OP_LABEL_CHANGE (type 5) operation.""" return { 'type': 5, 'timestamp': timestamp, # git-bug serializes the unused side of a label change as null. 'added': list(added) if added is not None else None, 'removed': list(removed) if removed is not None else None, } def make_set_metadata_op( metadata: JsonObject, timestamp: int = 1700005000 ) -> JsonObject: """Build an OP_SET_METADATA (type 8) operation.""" return {'type': 8, 'timestamp': timestamp, 'new_metadata': metadata} def make_noop_op(timestamp: int = 1700006000) -> JsonObject: """Build an OP_NOOP (type 7) operation.""" return {'type': 7, 'timestamp': timestamp} # -- Real git object helpers ------------------------------------------------- def _create_bug_commit( repo: pygit2.Repository, refname: str, ops_json: str, parent_oid: pygit2.Oid | None = None, version_tag: str = 'version-4', ) -> pygit2.Oid: """Create a single bug commit with an ops blob and version marker. If *parent_oid* is given the new commit is chained after it. Returns the new commit OID. """ ops_blob = repo.create_blob(ops_json.encode()) version_blob = repo.create_blob(b'') tb = repo.TreeBuilder() tb.insert('ops', ops_blob, pygit2.GIT_FILEMODE_BLOB) tb.insert(version_tag, version_blob, pygit2.GIT_FILEMODE_BLOB) tree_oid = tb.write() parents = [parent_oid] if parent_oid else [] return repo.create_commit( refname, _SIG, _SIG, 'op pack', tree_oid, parents, ) def _create_identity_commit( repo: pygit2.Repository, refname: str, version_json: str ) -> pygit2.Oid: """Create an identity commit with a ``version`` blob. Returns the commit OID. """ version_blob = repo.create_blob(version_json.encode()) tb = repo.TreeBuilder() tb.insert('version', version_blob, pygit2.GIT_FILEMODE_BLOB) tree_oid = tb.write() return repo.create_commit( refname, _SIG, _SIG, 'identity', tree_oid, [], ) # -- Convenience setup ------------------------------------------------------- def setup_identity(repo_path: str, identity_id: str, name: str, email: str) -> None: """Create real identity git objects in the repo at *repo_path*. Writes a ``refs/identities/`` ref pointing at a commit whose tree contains a ``version`` blob with the identity JSON. """ repo = pygit2.Repository(str(repo_path)) version_json = make_identity_version(name, email) refname = 'refs/identities/%s' % identity_id _create_identity_commit(repo, refname, version_json) def setup_single_bug( repo_path: str, reader: BugReader, bid: str = BUG_ID, title: str = 'Test bug', message: str = 'Bug description', timestamp: int = 1700000000, extra_ops: list[JsonObject] | None = None, author_id: str = IDENTITY_ID, extra_packs: list[str] | None = None, ) -> None: """Create real git objects for a single bug with one create op. The bug ref ``refs/bugs/`` is created in the repo at *repo_path*. The identity for *author_id* is also created. If *extra_packs* is given, each element is a JSON string that becomes an additional chained commit (for multi-pack tests). """ repo = pygit2.Repository(str(repo_path)) refname = 'refs/bugs/%s' % bid ops = [make_create_op(title, message, timestamp)] if extra_ops: ops.extend(extra_ops) pack_json = make_op_pack(author_id, ops) parent = _create_bug_commit(repo, refname, pack_json) if extra_packs: for extra_json in extra_packs: parent = _create_bug_commit( repo, refname, extra_json, parent_oid=parent, ) # Pre-cache bug ID resolution so tests don't need separate # ref enumeration setup. reader._resolve_cache[bid] = bid # Identity resolution setup_identity(repo_path, author_id, 'Alice', 'alice@example.com') # -- Fixtures ---------------------------------------------------------------- class RecordingBugWriter(BugWriter): """BugWriter test double that records CLI calls and canned responses.""" def __init__(self, repo_path: str, reader: BugReader) -> None: super().__init__(repo_path, reader) self._cli_calls: list[list[str]] = [] self._cli_routes: dict[str, CliResult] = {} @override def _cli( self, args: list[str], stdin: str | None = None, ) -> CliResult: self._cli_calls.append(args) joined = ' '.join(args) for key, value in self._cli_routes.items(): if key in joined: return value return (0, '', '') @pytest.fixture() def repo_path(tmp_path: Path) -> str: """Create a bare git repo and return its path as a string.""" repo_dir = tmp_path / 'repo' pygit2.init_repository(str(repo_dir), bare=True) return str(repo_dir) @pytest.fixture() def reader(repo_path: str) -> BugReader: """Create a BugReader pointing at the test repo.""" return BugReader(repo_path) @pytest.fixture() def writer(reader: BugReader, repo_path: str) -> RecordingBugWriter: """Create a BugWriter with git_bug_cli mocked. Write operations shell out to the ``git bug`` CLI, which isn't available in tests, so we mock it. The mock captures calls in ``writer._cli_calls`` for assertion. """ return RecordingBugWriter(repo_path, reader) python-ezgb-0.2.1/tests/test_ezgb.py000066400000000000000000001062401526117125300174700ustar00rootroot00000000000000"""Tests for ezgb, ported from bugspray's test_gitbug.py. Covers the reader (git object parsing, op-pack replay, caching), writer (CLI-based mutations), and GitBugRepo facade. Uses real git objects in temporary bare repos (via pygit2) instead of subprocess mocking. See conftest.py for fixtures and helpers. """ from __future__ import annotations import json from datetime import datetime, timezone import pygit2 import pytest from conftest import ( BUG_ID, IDENTITY_ID, RecordingBugWriter, _create_bug_commit, _create_identity_commit, make_comment_op, make_create_op, make_edit_comment_op, make_label_change_op, make_noop_op, make_op_pack, make_set_metadata_op, make_set_status_op, make_set_title_op, setup_identity, setup_single_bug, ) from ezgb import ( AmbiguousBugIdError, BugNotFoundError, BugSummary, GitBugRepo, Status, UnsupportedFormatError, ) from ezgb._reader import BugReader, _combine_ids from ezgb._types import JsonObject # ------------------------------------------------------------------ # Unit tests: _combine_ids # ------------------------------------------------------------------ class TestCombineIds: """Unit tests for the CombinedId interleaving function.""" def test_interleave_pattern(self) -> None: primary = '0' * 64 secondary = '1' * 64 result = _combine_ids(primary, secondary) assert len(result) == 64 # Secondary chars at positions 1, 3, 5, 9 and i>=10 where i%5==4 for i, ch in enumerate(result): if i in (1, 3, 5, 9) or (i >= 10 and i % 5 == 4): assert ch == '1', 'position %d should be secondary' % i else: assert ch == '0', 'position %d should be primary' % i def test_distinct_inputs(self) -> None: primary = 'abcdef' * 10 + 'abcd' secondary = '123456' * 10 + '1234' result = _combine_ids(primary, secondary) assert len(result) == 64 assert any(c in 'abcdef' for c in result) assert any(c in '123456' for c in result) # ------------------------------------------------------------------ # Bug ID resolution # ------------------------------------------------------------------ class TestResolveBugId: def test_resolves_full_id(self, reader: BugReader, repo_path: str) -> None: repo = pygit2.Repository(repo_path) ops_json = make_op_pack(IDENTITY_ID, [make_create_op('t', 'm')]) _create_bug_commit(repo, 'refs/bugs/%s' % BUG_ID, ops_json) result = reader.resolve_bug_id(BUG_ID) assert result == BUG_ID def test_resolves_prefix(self, reader: BugReader, repo_path: str) -> None: repo = pygit2.Repository(repo_path) ops_json = make_op_pack(IDENTITY_ID, [make_create_op('t', 'm')]) _create_bug_commit(repo, 'refs/bugs/%s' % BUG_ID, ops_json) prefix = BUG_ID[:7] result = reader.resolve_bug_id(prefix) assert result == BUG_ID def test_caches_result(self, reader: BugReader, repo_path: str) -> None: repo = pygit2.Repository(repo_path) ops_json = make_op_pack(IDENTITY_ID, [make_create_op('t', 'm')]) _create_bug_commit(repo, 'refs/bugs/%s' % BUG_ID, ops_json) reader.resolve_bug_id(BUG_ID) # Second call should use cache result = reader.resolve_bug_id(BUG_ID) assert result == BUG_ID def test_ambiguous_raises(self, reader: BugReader, repo_path: str) -> None: repo = pygit2.Repository(repo_path) prefix = 'abc' bid2 = 'abc' + 'd' * 61 bid3 = 'abc' + 'e' * 61 ops_json = make_op_pack(IDENTITY_ID, [make_create_op('t', 'm')]) _create_bug_commit(repo, 'refs/bugs/%s' % bid2, ops_json) _create_bug_commit(repo, 'refs/bugs/%s' % bid3, ops_json) with pytest.raises(AmbiguousBugIdError, match='matches 2 bugs'): reader.resolve_bug_id(prefix) def test_not_found_raises(self, reader: BugReader, repo_path: str) -> None: with pytest.raises(BugNotFoundError, match='no bug matching'): reader.resolve_bug_id('nonexistent') # ------------------------------------------------------------------ # Identity resolution # ------------------------------------------------------------------ class TestResolveIdentity: def test_resolves_by_id(self, reader: BugReader, repo_path: str) -> None: setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') identity = reader.resolve_identity(IDENTITY_ID) assert identity.name == 'Alice' assert identity.email == 'alice@example.com' assert identity.login == 'alice' assert identity.id == IDENTITY_ID def test_caches_result(self, reader: BugReader, repo_path: str) -> None: setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') id1 = reader.resolve_identity(IDENTITY_ID) id2 = reader.resolve_identity(IDENTITY_ID) assert id1 is id2 def test_fallback_on_missing(self, reader: BugReader, repo_path: str) -> None: identity = reader.resolve_identity(IDENTITY_ID) assert identity.name == IDENTITY_ID assert identity.email == IDENTITY_ID def test_unsupported_format_raises(self, reader: BugReader, repo_path: str) -> None: repo = pygit2.Repository(repo_path) identity_json = json.dumps( { 'version': 99, 'name': 'Alice', 'email': 'alice@example.com', } ) refname = 'refs/identities/%s' % IDENTITY_ID _create_identity_commit(repo, refname, identity_json) with pytest.raises(UnsupportedFormatError, match='identity format'): reader.resolve_identity(IDENTITY_ID) # ------------------------------------------------------------------ # Bug snapshot reconstruction # ------------------------------------------------------------------ class TestBuildBug: def test_snapshot_reconstruction(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) bug = reader.build_bug(BUG_ID) assert bug.id == BUG_ID assert bug.title == 'Test bug' assert bug.status == Status.OPEN assert bug.creator.name == 'Alice' assert bug.creator.email == 'alice@example.com' assert len(bug.comments) == 1 assert bug.comments[0].text == 'Bug description' assert bug.comments[0].count == 0 def test_set_title_updates(self, reader: BugReader, repo_path: str) -> None: title_op = make_set_title_op('Updated title') setup_single_bug(repo_path, reader, extra_ops=[title_op]) bug = reader.build_bug(BUG_ID) assert bug.title == 'Updated title' def test_set_status_closed(self, reader: BugReader, repo_path: str) -> None: status_op = make_set_status_op(2) # STATUS_CLOSED setup_single_bug(repo_path, reader, extra_ops=[status_op]) bug = reader.build_bug(BUG_ID) assert bug.status == Status.CLOSED def test_assigned_label(self, reader: BugReader, repo_path: str) -> None: assign_op = make_label_change_op(added=['assigned:bob@example.com']) setup_single_bug(repo_path, reader, extra_ops=[assign_op]) bug = reader.build_bug(BUG_ID) assert 'assigned:bob@example.com' in bug.labels def test_label_add_and_remove(self, reader: BugReader, repo_path: str) -> None: add_op = make_label_change_op(added=['bug', 'priority/high']) rm_op = make_label_change_op( removed=['bug'], timestamp=1700005000, ) setup_single_bug(repo_path, reader, extra_ops=[add_op, rm_op]) bug = reader.build_bug(BUG_ID) assert 'priority/high' in bug.labels assert 'bug' not in bug.labels def test_add_comment_op(self, reader: BugReader, repo_path: str) -> None: comment_op = make_comment_op('A follow-up', timestamp=1700001000) setup_single_bug(repo_path, reader, extra_ops=[comment_op]) bug = reader.build_bug(BUG_ID) assert len(bug.comments) == 2 assert bug.comments[0].count == 0 assert bug.comments[0].text == 'Bug description' assert bug.comments[1].count == 1 assert bug.comments[1].text == 'A follow-up' def test_comment_with_attachment(self, reader: BugReader, repo_path: str) -> None: ops: list[JsonObject] = [ { 'type': 1, 'timestamp': 1700000000, 'title': 'Bug with file', 'message': 'See attached', 'files': ['blobhash123'], } ] setup_single_bug( repo_path, reader, title='Bug with file', message='See attached', extra_ops=None, ) # Rebuild with custom ops containing files reader.invalidate(BUG_ID) repo = pygit2.Repository(repo_path) # Delete old ref and create new one with files in the op repo.references.delete('refs/bugs/%s' % BUG_ID) pack_json = make_op_pack(IDENTITY_ID, ops) _create_bug_commit(repo, 'refs/bugs/%s' % BUG_ID, pack_json) reader._resolve_cache[BUG_ID] = BUG_ID bug = reader.build_bug(BUG_ID) assert bug.comments[0].attachment_ids == ['blobhash123'] def test_edit_comment(self, reader: BugReader, repo_path: str) -> None: """OP_EDIT_COMMENT whose target matches the create op hash should update the comment text.""" # First, build the pack JSON for the create op so we can # compute the correct hash from the raw serialized op. create_op = make_create_op('Test bug', 'Original text') pack_json = make_op_pack(IDENTITY_ID, [create_op]) # Extract the raw op JSON string from the pack raw_ops = BugReader._extract_raw_ops(pack_json) target_hash = BugReader._op_hash(raw_ops[0]) edit_op = make_edit_comment_op( target=target_hash, message='Edited text', ) # Build the full pack with both ops ops = [create_op, edit_op] full_pack_json = make_op_pack(IDENTITY_ID, ops) repo = pygit2.Repository(repo_path) _create_bug_commit( repo, 'refs/bugs/%s' % BUG_ID, full_pack_json, ) reader._resolve_cache[BUG_ID] = BUG_ID setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') bug = reader.build_bug(BUG_ID) assert bug.comments[0].text == 'Edited text' def test_edit_comment_unmatched_target( self, reader: BugReader, repo_path: str ) -> None: """Unmatched OP_EDIT_COMMENT target leaves text unchanged.""" edit_op = make_edit_comment_op( target='nonexistent', message='Edited text', ) setup_single_bug(repo_path, reader, extra_ops=[edit_op]) bug = reader.build_bug(BUG_ID) assert bug.comments[0].text == 'Bug description' def test_metadata_from_set_metadata( self, reader: BugReader, repo_path: str ) -> None: meta_op = make_set_metadata_op({'key': 'value'}) setup_single_bug(repo_path, reader, extra_ops=[meta_op]) bug = reader.build_bug(BUG_ID) assert bug.metadata == {'key': 'value'} def test_noop_ignored(self, reader: BugReader, repo_path: str) -> None: noop = make_noop_op() setup_single_bug(repo_path, reader, extra_ops=[noop]) bug = reader.build_bug(BUG_ID) assert bug.title == 'Test bug' def test_multiple_op_packs(self, reader: BugReader, repo_path: str) -> None: """Operations spread across multiple commits are replayed in order.""" pack2_json = make_op_pack( IDENTITY_ID, [ make_set_title_op('Updated'), ], ) setup_single_bug( repo_path, reader, title='Original', message='Body', extra_packs=[pack2_json], ) bug = reader.build_bug(BUG_ID) assert bug.title == 'Updated' def test_missing_bug_raises(self, reader: BugReader, repo_path: str) -> None: reader._resolve_cache['nonexistent'] = 'nonexistent' with pytest.raises(BugNotFoundError, match='no operation packs'): reader.build_bug('nonexistent') def test_caching(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) bug1 = reader.build_bug(BUG_ID) bug2 = reader.build_bug(BUG_ID) assert bug1 is bug2 def test_unsupported_format_raises(self, reader: BugReader, repo_path: str) -> None: repo = pygit2.Repository(repo_path) ops_json = make_op_pack( IDENTITY_ID, [ make_create_op('Test', 'body'), ], ) _create_bug_commit( repo, 'refs/bugs/%s' % BUG_ID, ops_json, version_tag='version-99', ) reader._resolve_cache[BUG_ID] = BUG_ID with pytest.raises(UnsupportedFormatError): reader.build_bug(BUG_ID) # ------------------------------------------------------------------ # Bug summary (lightweight list-view snapshot) # ------------------------------------------------------------------ class TestBuildBugSummary: def test_basic(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) s = reader.build_bug_summary(BUG_ID) assert isinstance(s, BugSummary) assert s.id == BUG_ID assert s.title == 'Test bug' assert s.status == Status.OPEN assert s.creator_id == IDENTITY_ID assert s.comment_count == 1 # create op has a message def test_author_name_resolved(self, reader: BugReader, repo_path: str) -> None: # The native path must resolve the creator's display name so # list views match summaries from the git-bug CLI cache. setup_single_bug(repo_path, reader) s = reader.build_bug_summary(BUG_ID) assert s.author_name == 'Alice' def test_edited_at_tracks_latest_op( self, reader: BugReader, repo_path: str ) -> None: comment_op = make_comment_op('A follow-up', timestamp=1700009000) setup_single_bug(repo_path, reader, extra_ops=[comment_op]) s = reader.build_bug_summary(BUG_ID) assert s.edited_at == datetime.fromtimestamp(1700009000, tz=timezone.utc) assert s.edited_at > s.created_at def test_edited_at_defaults_to_created( self, reader: BugReader, repo_path: str ) -> None: setup_single_bug(repo_path, reader) s = reader.build_bug_summary(BUG_ID) assert s.edited_at == s.created_at def test_set_title(self, reader: BugReader, repo_path: str) -> None: title_op = make_set_title_op('Updated title') setup_single_bug(repo_path, reader, extra_ops=[title_op]) s = reader.build_bug_summary(BUG_ID) assert s.title == 'Updated title' def test_set_status(self, reader: BugReader, repo_path: str) -> None: status_op = make_set_status_op(2) # CLOSED setup_single_bug(repo_path, reader, extra_ops=[status_op]) s = reader.build_bug_summary(BUG_ID) assert s.status == Status.CLOSED def test_label_changes(self, reader: BugReader, repo_path: str) -> None: add_op = make_label_change_op(added=['bug', 'priority/high']) rm_op = make_label_change_op( removed=['bug'], timestamp=1700005000, ) setup_single_bug(repo_path, reader, extra_ops=[add_op, rm_op]) s = reader.build_bug_summary(BUG_ID) assert 'priority/high' in s.labels assert 'bug' not in s.labels assert isinstance(s.labels, frozenset) def test_comment_count(self, reader: BugReader, repo_path: str) -> None: comment_op = make_comment_op('A follow-up') setup_single_bug(repo_path, reader, extra_ops=[comment_op]) s = reader.build_bug_summary(BUG_ID) assert s.comment_count == 2 # create message + add_comment def test_create_without_message(self, reader: BugReader, repo_path: str) -> None: repo = pygit2.Repository(repo_path) ops = [make_create_op('No body', '')] pack_json = make_op_pack(IDENTITY_ID, ops) _create_bug_commit(repo, 'refs/bugs/%s' % BUG_ID, pack_json) reader._resolve_cache[BUG_ID] = BUG_ID setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') s = reader.build_bug_summary(BUG_ID) assert s.comment_count == 0 def test_edit_comment_does_not_affect_count( self, reader: BugReader, repo_path: str ) -> None: edit_op = make_edit_comment_op( target='whatever', message='Edited', ) setup_single_bug(repo_path, reader, extra_ops=[edit_op]) s = reader.build_bug_summary(BUG_ID) assert s.comment_count == 1 # only the create message def test_metadata_skipped(self, reader: BugReader, repo_path: str) -> None: meta_op = make_set_metadata_op({'key': 'value'}) setup_single_bug(repo_path, reader, extra_ops=[meta_op]) s = reader.build_bug_summary(BUG_ID) assert not hasattr(s, 'metadata') def test_noop_ignored(self, reader: BugReader, repo_path: str) -> None: noop = make_noop_op() setup_single_bug(repo_path, reader, extra_ops=[noop]) s = reader.build_bug_summary(BUG_ID) assert s.title == 'Test bug' def test_caching(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) s1 = reader.build_bug_summary(BUG_ID) s2 = reader.build_bug_summary(BUG_ID) assert s1 is s2 def test_separate_cache(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) reader.build_bug_summary(BUG_ID) assert BUG_ID in reader._summary_cache assert BUG_ID not in reader._bug_cache def test_missing_raises(self, reader: BugReader, repo_path: str) -> None: reader._resolve_cache['nonexistent'] = 'nonexistent' with pytest.raises(BugNotFoundError, match='no operation packs'): reader.build_bug_summary('nonexistent') def test_invalidate_single(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) reader.build_bug_summary(BUG_ID) assert BUG_ID in reader._summary_cache reader.invalidate(BUG_ID) assert BUG_ID not in reader._summary_cache def test_invalidate_all(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) reader.build_bug_summary(BUG_ID) reader.invalidate() assert reader._summary_cache == {} # ------------------------------------------------------------------ # Prefetch # ------------------------------------------------------------------ class TestPrefetchBugs: def test_warms_cache(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) reader._bug_cache.clear() assert BUG_ID not in reader._bug_cache reader.prefetch_bugs([BUG_ID]) assert BUG_ID in reader._bug_cache assert reader._bug_cache[BUG_ID].title == 'Test bug' def test_skips_cached(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) bug = reader.build_bug(BUG_ID) reader.prefetch_bugs([BUG_ID]) assert reader._bug_cache[BUG_ID] is bug def test_empty_list(self, reader: BugReader, repo_path: str) -> None: reader.prefetch_bugs([]) assert reader._bug_cache == {} # ------------------------------------------------------------------ # Ref enumeration # ------------------------------------------------------------------ class TestListBugRefs: def test_returns_refs(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) refs = reader.list_bug_refs() assert len(refs) == 1 assert refs[0][0] == BUG_ID def test_empty(self, reader: BugReader, repo_path: str) -> None: refs = reader.list_bug_refs() assert refs == [] class TestListIdentityRefs: def test_returns_refs(self, reader: BugReader, repo_path: str) -> None: setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') refs = reader.list_identity_refs() assert len(refs) == 1 assert refs[0][0] == IDENTITY_ID # ------------------------------------------------------------------ # Cache management # ------------------------------------------------------------------ class TestInvalidate: def test_invalidate_single_bug(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) reader.build_bug(BUG_ID) assert BUG_ID in reader._bug_cache reader.invalidate(BUG_ID) assert BUG_ID not in reader._bug_cache # Resolve cache preserved for single-bug invalidation assert BUG_ID in reader._resolve_cache def test_invalidate_all(self, reader: BugReader, repo_path: str) -> None: setup_single_bug(repo_path, reader) reader.build_bug(BUG_ID) reader.invalidate() assert reader._bug_cache == {} assert reader._identity_cache == {} assert reader._resolve_cache == {} # ------------------------------------------------------------------ # Writer: create_bug # ------------------------------------------------------------------ class TestCreateBug: def test_creates_and_returns_bug( self, reader: BugReader, writer: RecordingBugWriter, repo_path: str, ) -> None: new_id = 'd' * 64 prefix = new_id[:7] # CLI returns the created message writer._cli_routes['bug new'] = (0, '%s created\n' % prefix, '') # After create, invalidate() clears resolve cache, so we # need real git objects for resolution and building. repo = pygit2.Repository(repo_path) ops = [make_create_op('New bug', 'Details')] pack_json = make_op_pack(IDENTITY_ID, ops) _create_bug_commit(repo, 'refs/bugs/%s' % new_id, pack_json) setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') bug = writer.create_bug('New bug', 'Details') assert bug.id == new_id assert bug.title == 'New bug' assert bug.status == Status.OPEN # ------------------------------------------------------------------ # Writer: add_comment # ------------------------------------------------------------------ class TestAddComment: def test_adds_and_returns_comment( self, reader: BugReader, writer: RecordingBugWriter, repo_path: str, ) -> None: setup_single_bug(repo_path, reader) # CLI returns success writer._cli_routes['comment new'] = (0, 'comment added\n', '') # After cache invalidation, the rebuild reads updated ops. # Chain a second commit with the comment op onto the existing # bug ref (the first commit already has the create op). repo = pygit2.Repository(repo_path) ref = repo.references.get('refs/bugs/%s' % BUG_ID) assert ref is not None parent = ref.peel(pygit2.Commit) comment_op = make_comment_op('New comment', timestamp=1700005000) pack_json = make_op_pack(IDENTITY_ID, [comment_op]) _create_bug_commit( repo, 'refs/bugs/%s' % BUG_ID, pack_json, parent_oid=parent.id, ) comment = writer.add_comment(BUG_ID, 'New comment') assert comment.text == 'New comment' assert comment.count == 1 # ------------------------------------------------------------------ # Writer: set_status # ------------------------------------------------------------------ class TestSetStatus: def test_close( self, reader: BugReader, writer: RecordingBugWriter, repo_path: str, ) -> None: setup_single_bug(repo_path, reader) writer.set_status(BUG_ID, Status.CLOSED) assert any('close' in ' '.join(c) for c in writer._cli_calls) def test_open( self, reader: BugReader, writer: RecordingBugWriter, repo_path: str, ) -> None: setup_single_bug(repo_path, reader) writer.set_status(BUG_ID, Status.OPEN) assert any('open' in ' '.join(c) for c in writer._cli_calls) # ------------------------------------------------------------------ # Writer: set_title # ------------------------------------------------------------------ class TestSetTitle: def test_updates_title( self, reader: BugReader, writer: RecordingBugWriter, repo_path: str, ) -> None: setup_single_bug(repo_path, reader) writer.set_title(BUG_ID, 'New title') assert any('New title' in ' '.join(c) for c in writer._cli_calls) # ------------------------------------------------------------------ # Writer: labels # ------------------------------------------------------------------ class TestLabels: def test_add_label( self, reader: BugReader, writer: RecordingBugWriter, repo_path: str, ) -> None: setup_single_bug(repo_path, reader) writer.add_label(BUG_ID, 'priority/high') assert any('priority/high' in ' '.join(c) for c in writer._cli_calls) def test_remove_label( self, reader: BugReader, writer: RecordingBugWriter, repo_path: str, ) -> None: setup_single_bug(repo_path, reader) writer.remove_label(BUG_ID, 'old-label') assert any('old-label' in ' '.join(c) for c in writer._cli_calls) # ------------------------------------------------------------------ # ------------------------------------------------------------------ # GitBugRepo facade # ------------------------------------------------------------------ class TestGitBugRepo: def test_get_bug(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) setup_single_bug(repo_path, repo_obj._reader) bug = repo_obj.get_bug(BUG_ID) assert bug.title == 'Test bug' def test_list_bugs(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) setup_single_bug(repo_path, repo_obj._reader) bugs = repo_obj.list_bugs() assert len(bugs) == 1 assert bugs[0].title == 'Test bug' def test_list_bugs_filter_status(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) status_op = make_set_status_op(2) # CLOSED setup_single_bug(repo_path, repo_obj._reader, extra_ops=[status_op]) bugs = repo_obj.list_bugs(status=Status.OPEN) assert len(bugs) == 0 repo_obj.invalidate() bugs = repo_obj.list_bugs(status=Status.CLOSED) assert len(bugs) == 1 def test_list_bugs_filter_label(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) label_op = make_label_change_op(added=['area/network']) setup_single_bug(repo_path, repo_obj._reader, extra_ops=[label_op]) bugs = repo_obj.list_bugs(label='area/network') assert len(bugs) == 1 repo_obj.invalidate() bugs = repo_obj.list_bugs(label='nonexistent') assert len(bugs) == 0 def test_resolve_bug_id(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) repo = pygit2.Repository(repo_path) ops_json = make_op_pack(IDENTITY_ID, [make_create_op('t', 'm')]) _create_bug_commit(repo, 'refs/bugs/%s' % BUG_ID, ops_json) result = repo_obj.resolve_bug_id(BUG_ID) assert result == BUG_ID def test_list_identities(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') identities = repo_obj.list_identities() assert len(identities) == 1 assert identities[0].name == 'Alice' def test_iter_bugs(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) setup_single_bug(repo_path, repo_obj._reader) bugs = list(repo_obj.iter_bugs()) assert len(bugs) == 1 assert bugs[0].title == 'Test bug' def test_iter_bugs_is_lazy(self, repo_path: str) -> None: """iter_bugs yields one at a time without prefetching all.""" repo_obj = GitBugRepo(repo_path) setup_single_bug(repo_path, repo_obj._reader) it = repo_obj.iter_bugs() # Nothing built yet assert BUG_ID not in repo_obj._reader._bug_cache bug = next(it) assert bug.title == 'Test bug' def test_list_bug_summaries(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) setup_single_bug(repo_path, repo_obj._reader) summaries = repo_obj.list_bug_summaries() assert len(summaries) == 1 assert isinstance(summaries[0], BugSummary) assert summaries[0].title == 'Test bug' assert summaries[0].creator_id == IDENTITY_ID def test_list_bug_summaries_filter_status(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) status_op = make_set_status_op(2) # CLOSED setup_single_bug(repo_path, repo_obj._reader, extra_ops=[status_op]) assert len(repo_obj.list_bug_summaries(status=Status.OPEN)) == 0 repo_obj.invalidate() assert len(repo_obj.list_bug_summaries(status=Status.CLOSED)) == 1 def test_list_bug_summaries_filter_label(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) label_op = make_label_change_op(added=['area/network']) setup_single_bug(repo_path, repo_obj._reader, extra_ops=[label_op]) assert len(repo_obj.list_bug_summaries(label='area/network')) == 1 repo_obj.invalidate() assert len(repo_obj.list_bug_summaries(label='nonexistent')) == 0 def test_iter_bug_summaries(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) setup_single_bug(repo_path, repo_obj._reader) summaries = list(repo_obj.iter_bug_summaries()) assert len(summaries) == 1 assert summaries[0].title == 'Test bug' def test_get_attachment(self, repo_path: str) -> None: repo_obj = GitBugRepo(repo_path) repo = pygit2.Repository(repo_path) blob_oid = repo.create_blob(b'file contents here') data = repo_obj.get_attachment(str(blob_oid)) assert data == b'file contents here' def test_list_bugs_since(self, repo_path: str) -> None: """list_bugs(since=...) filters by committer timestamp.""" repo_obj = GitBugRepo(repo_path) # Create bug with a specific commit timestamp repo = pygit2.Repository(repo_path) sig = pygit2.Signature('Test', 'test@test.com', time=1700005000, offset=0) ops = [make_create_op('Test bug', 'Bug description')] pack_json = make_op_pack(IDENTITY_ID, ops) ops_blob = repo.create_blob(pack_json.encode()) version_blob = repo.create_blob(b'') tb = repo.TreeBuilder() tb.insert('ops', ops_blob, pygit2.GIT_FILEMODE_BLOB) tb.insert('version-4', version_blob, pygit2.GIT_FILEMODE_BLOB) tree_oid = tb.write() repo.create_commit( 'refs/bugs/%s' % BUG_ID, sig, sig, 'op pack', tree_oid, [], ) repo_obj._reader._resolve_cache[BUG_ID] = BUG_ID setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') # since before the commit -- should match bugs = repo_obj.list_bugs(since=1700000000) assert len(bugs) == 1 # since after the commit -- should not match repo_obj.invalidate() # Re-populate resolve cache after invalidation repo_obj._reader._resolve_cache[BUG_ID] = BUG_ID bugs = repo_obj.list_bugs(since=1700010000) assert len(bugs) == 0 def test_list_bugs_since_string(self, repo_path: str) -> None: """list_bugs(since='2023-11-14 ...') parses the string.""" repo_obj = GitBugRepo(repo_path) repo = pygit2.Repository(repo_path) sig = pygit2.Signature('Test', 'test@test.com', time=1700005000, offset=0) ops = [make_create_op('Test bug', 'Bug description')] pack_json = make_op_pack(IDENTITY_ID, ops) ops_blob = repo.create_blob(pack_json.encode()) version_blob = repo.create_blob(b'') tb = repo.TreeBuilder() tb.insert('ops', ops_blob, pygit2.GIT_FILEMODE_BLOB) tb.insert('version-4', version_blob, pygit2.GIT_FILEMODE_BLOB) tree_oid = tb.write() repo.create_commit( 'refs/bugs/%s' % BUG_ID, sig, sig, 'op pack', tree_oid, [], ) repo_obj._reader._resolve_cache[BUG_ID] = BUG_ID setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com') bugs = repo_obj.list_bugs(since='2023-11-14 00:00:00') assert len(bugs) == 1 # ------------------------------------------------------------------ # Attachment reading # ------------------------------------------------------------------ class TestCatBlobBytes: def test_reads_bytes(self, reader: BugReader, repo_path: str) -> None: repo = pygit2.Repository(repo_path) blob_oid = repo.create_blob(b'binary content') data = reader.cat_blob_bytes(str(blob_oid)) assert data == b'binary content' assert isinstance(data, bytes) def test_missing_blob_raises(self, reader: BugReader, repo_path: str) -> None: with pytest.raises(BugNotFoundError): reader.cat_blob_bytes('ff' * 20) # ------------------------------------------------------------------ # Since filtering on list_bug_refs # ------------------------------------------------------------------ class TestListBugRefsSince: def _create_bug_with_timestamp( self, repo_path: str, reader: BugReader, ts: int ) -> None: """Helper: create a bug ref with a specific commit timestamp.""" repo = pygit2.Repository(repo_path) sig = pygit2.Signature('Test', 'test@test.com', time=ts, offset=0) ops = [make_create_op('Test bug', 'description')] pack_json = make_op_pack(IDENTITY_ID, ops) ops_blob = repo.create_blob(pack_json.encode()) version_blob = repo.create_blob(b'') tb = repo.TreeBuilder() tb.insert('ops', ops_blob, pygit2.GIT_FILEMODE_BLOB) tb.insert('version-4', version_blob, pygit2.GIT_FILEMODE_BLOB) tree_oid = tb.write() repo.create_commit( 'refs/bugs/%s' % BUG_ID, sig, sig, 'op pack', tree_oid, [], ) def test_no_filter(self, reader: BugReader, repo_path: str) -> None: self._create_bug_with_timestamp(repo_path, reader, 1700005000) refs = reader.list_bug_refs() assert len(refs) == 1 def test_since_includes_newer(self, reader: BugReader, repo_path: str) -> None: self._create_bug_with_timestamp(repo_path, reader, 1700005000) refs = reader.list_bug_refs(since=1700000000) assert len(refs) == 1 def test_since_excludes_older(self, reader: BugReader, repo_path: str) -> None: self._create_bug_with_timestamp(repo_path, reader, 1700005000) refs = reader.list_bug_refs(since=1700010000) assert len(refs) == 0 python-ezgb-0.2.1/tests/test_integration.py000066400000000000000000000265741526117125300210770ustar00rootroot00000000000000"""Integration tests using real git repos and the git-bug binary. These tests create ephemeral git repositories, initialize git-bug with a test identity, and exercise the full ezgb stack end-to-end. Skip automatically when ``git-bug`` is not installed. """ from __future__ import annotations import shutil import subprocess from pathlib import Path import pytest from ezgb import GitBugRepo, Status from ezgb._types import JsonValue, json_loads pytestmark = pytest.mark.skipif( shutil.which('git-bug') is None, reason='git-bug binary not found', ) @pytest.fixture() def gb_repo(tmp_path: Path) -> GitBugRepo: """Spin up a temporary git repo with git-bug and a test identity.""" repo_dir = tmp_path / 'repo' repo_dir.mkdir() repo_path = str(repo_dir) def _run(args: list[str]) -> subprocess.CompletedProcess[str]: return subprocess.run( args, capture_output=True, text=True, check=True, ) # Initialise the git repo _run(['git', 'init', repo_path]) _run(['git', '-C', repo_path, 'config', 'user.name', 'Test User']) _run(['git', '-C', repo_path, 'config', 'user.email', 'test@example.com']) _run(['git', '-C', repo_path, 'commit', '--allow-empty', '-m', 'init']) # Create and adopt a git-bug identity _run( [ 'git', '-C', repo_path, 'bug', 'user', 'new', '-n', 'Test User', '-e', 'test@example.com', '--non-interactive', ] ) result = _run(['git', '-C', repo_path, 'bug', 'user', '-f', 'json']) users: JsonValue = json_loads(result.stdout) assert isinstance(users, list) user = users[0] assert isinstance(user, dict) user_id = user['id'] assert isinstance(user_id, str) _run(['git', '-C', repo_path, 'bug', 'user', 'adopt', user_id]) return GitBugRepo(repo_path) # ------------------------------------------------------------------ # Tests # ------------------------------------------------------------------ class TestCreateAndRead: def test_round_trip(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Test bug', 'Description text') assert bug.title == 'Test bug' assert bug.status == Status.OPEN assert len(bug.comments) == 1 assert bug.comments[0].text == 'Description text' assert bug.creator.name == 'Test User' assert bug.creator.email == 'test@example.com' assert len(bug.id) == 64 # Read back through the reader path gb_repo.invalidate() bug2 = gb_repo.get_bug(bug.id) assert bug2.id == bug.id assert bug2.title == 'Test bug' def test_resolve_by_prefix(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Prefix test', 'Body') prefix = bug.id[:8] full_id = gb_repo.resolve_bug_id(prefix) assert full_id == bug.id class TestComments: def test_add_comment(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Comment test', 'Original body') comment = gb_repo.add_comment(bug.id, 'Follow-up') assert comment.text == 'Follow-up' assert comment.count == 1 bug = gb_repo.get_bug(bug.id) assert len(bug.comments) == 2 assert bug.comments[0].text == 'Original body' assert bug.comments[1].text == 'Follow-up' def test_multiple_comments_ordering(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Ordering test', 'c0') gb_repo.add_comment(bug.id, 'c1') gb_repo.add_comment(bug.id, 'c2') gb_repo.add_comment(bug.id, 'c3') bug = gb_repo.get_bug(bug.id) texts = [c.text for c in bug.comments] assert texts == ['c0', 'c1', 'c2', 'c3'] counts = [c.count for c in bug.comments] assert counts == [0, 1, 2, 3] class TestEditComment: def test_edit_comment_text(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Edit comment test', 'Original body') comment = gb_repo.add_comment(bug.id, 'Original follow-up') gb_repo.edit_comment(bug.id, comment.id, 'Edited follow-up') bug = gb_repo.get_bug(bug.id) assert len(bug.comments) == 2 assert bug.comments[1].text == 'Edited follow-up' def test_edit_preserves_other_comments(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Preserve test', 'c0') c1 = gb_repo.add_comment(bug.id, 'c1') gb_repo.add_comment(bug.id, 'c2') gb_repo.edit_comment(bug.id, c1.id, 'c1-edited') bug = gb_repo.get_bug(bug.id) texts = [c.text for c in bug.comments] assert texts == ['c0', 'c1-edited', 'c2'] def test_edit_to_tombstone(self, gb_repo: GitBugRepo) -> None: """Simulate the b4 bugs comment removal flow.""" bug = gb_repo.create_bug('Tombstone test', 'Body') comment = gb_repo.add_comment(bug.id, 'Sensitive content') tombstone = ( 'Message-ID: \n' 'X-B4-Bug-Comment: removed by Test User ' ) gb_repo.edit_comment(bug.id, comment.id, tombstone) bug = gb_repo.get_bug(bug.id) assert bug.comments[1].text == tombstone assert 'Sensitive content' not in bug.comments[1].text class TestStatus: def test_close_and_reopen(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Status test', 'Body') assert gb_repo.get_bug(bug.id).status == Status.OPEN gb_repo.set_status(bug.id, Status.CLOSED) bug = gb_repo.get_bug(bug.id) assert bug.status == Status.CLOSED gb_repo.set_status(bug.id, Status.OPEN) bug = gb_repo.get_bug(bug.id) assert bug.status == Status.OPEN class TestTitle: def test_edit_title(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Original title', 'Body') gb_repo.set_title(bug.id, 'Updated title') bug = gb_repo.get_bug(bug.id) assert bug.title == 'Updated title' class TestLabels: def test_add_and_remove(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Label test', 'Body') gb_repo.add_label(bug.id, 'priority/high') gb_repo.add_label(bug.id, 'area/network') bug = gb_repo.get_bug(bug.id) assert 'priority/high' in bug.labels assert 'area/network' in bug.labels gb_repo.remove_label(bug.id, 'priority/high') bug = gb_repo.get_bug(bug.id) assert 'priority/high' not in bug.labels assert 'area/network' in bug.labels class TestListBugs: def test_list_all(self, gb_repo: GitBugRepo) -> None: gb_repo.create_bug('Bug one', 'First') gb_repo.create_bug('Bug two', 'Second') bugs = gb_repo.list_bugs() assert len(bugs) == 2 titles = {b.title for b in bugs} assert titles == {'Bug one', 'Bug two'} def test_filter_by_status(self, gb_repo: GitBugRepo) -> None: gb_repo.create_bug('Open bug', 'Body') closed = gb_repo.create_bug('Closed bug', 'Body') gb_repo.set_status(closed.id, Status.CLOSED) gb_repo.invalidate() open_bugs = gb_repo.list_bugs(status=Status.OPEN) closed_bugs = gb_repo.list_bugs(status=Status.CLOSED) assert len(open_bugs) == 1 assert open_bugs[0].title == 'Open bug' assert len(closed_bugs) == 1 assert closed_bugs[0].title == 'Closed bug' def test_filter_by_label(self, gb_repo: GitBugRepo) -> None: labeled = gb_repo.create_bug('Labeled', 'Body') gb_repo.create_bug('Unlabeled', 'Body') gb_repo.add_label(labeled.id, 'important') gb_repo.invalidate() results = gb_repo.list_bugs(label='important') assert len(results) == 1 assert results[0].title == 'Labeled' class TestIdentities: def test_list_identities(self, gb_repo: GitBugRepo) -> None: identities = gb_repo.list_identities() assert len(identities) >= 1 names = [i.name for i in identities] assert 'Test User' in names emails = [i.email for i in identities] assert 'test@example.com' in emails class TestIterBugs: def test_yields_same_as_list(self, gb_repo: GitBugRepo) -> None: gb_repo.create_bug('Bug A', 'Body A') gb_repo.create_bug('Bug B', 'Body B') gb_repo.invalidate() listed = gb_repo.list_bugs() gb_repo.invalidate() iterated = list(gb_repo.iter_bugs()) assert len(listed) == len(iterated) == 2 assert {b.title for b in listed} == {b.title for b in iterated} def test_iter_with_filter(self, gb_repo: GitBugRepo) -> None: gb_repo.create_bug('Open one', 'Body') closed = gb_repo.create_bug('Closed one', 'Body') gb_repo.set_status(closed.id, Status.CLOSED) gb_repo.invalidate() open_bugs = list(gb_repo.iter_bugs(status=Status.OPEN)) assert len(open_bugs) == 1 assert open_bugs[0].title == 'Open one' class TestSinceFilter: def test_list_bugs_since(self, gb_repo: GitBugRepo) -> None: """Bugs created before 'since' are excluded.""" bug = gb_repo.create_bug('Recent bug', 'Body') # Use a timestamp far in the past -- should include the bug bugs = gb_repo.list_bugs(since=0) assert any(b.id == bug.id for b in bugs) # Use a timestamp far in the future -- should exclude gb_repo.invalidate() bugs = gb_repo.list_bugs(since=9999999999) assert len(bugs) == 0 def test_since_with_iso_string(self, gb_repo: GitBugRepo) -> None: gb_repo.create_bug('String since', 'Body') gb_repo.invalidate() bugs = gb_repo.list_bugs(since='2020-01-01 00:00:00') assert len(bugs) >= 1 class TestAttachments: def test_get_attachment_round_trip(self, gb_repo: GitBugRepo) -> None: """Create a bug with an attachment, then read it back.""" # Create a bug -- its description becomes comment 0 bug = gb_repo.create_bug('Attachment test', 'See attached') bug = gb_repo.get_bug(bug.id) # Comments might not have attachments in this simple case, # but we can at least verify get_attachment works on a known # blob. Use the ops blob itself as a stand-in. # Instead, test against an actual blob by writing one via git. import subprocess repo_path = gb_repo._repo result = subprocess.run( ['git', '-C', repo_path, 'hash-object', '-w', '--stdin'], input=b'test file content', capture_output=True, check=True, ) blob_hash = result.stdout.decode().strip() data = gb_repo.get_attachment(blob_hash) assert data == b'test file content' class TestCache: def test_invalidate_and_reread(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Cache test', 'Body') bug1 = gb_repo.get_bug(bug.id) gb_repo.invalidate(bug.id) bug2 = gb_repo.get_bug(bug.id) assert bug1 is not bug2 assert bug1.title == bug2.title assert bug1.id == bug2.id def test_full_invalidate(self, gb_repo: GitBugRepo) -> None: bug = gb_repo.create_bug('Full invalidate', 'Body') gb_repo.get_bug(bug.id) gb_repo.invalidate() # Should rebuild cleanly from git objects bug2 = gb_repo.get_bug(bug.id) assert bug2.title == 'Full invalidate' python-ezgb-0.2.1/uv.lock000066400000000000000000005261041526117125300152770ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", "python_full_version >= '3.11' and python_full_version < '3.15'", "python_full_version < '3.11'", ] [[package]] name = "ast-serialize" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" }, { url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" }, { url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" }, { url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" }, { url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" }, { url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" }, { url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" }, { url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" }, { url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" }, { url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" }, { url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" }, { url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" }, { url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" }, { url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" }, { url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" }, { url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" }, { url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" }, { url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" }, { url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" }, { url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" }, { url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" }, { url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" }, { url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" }, { url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" }, { url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" }, { url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" }, { url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" }, { url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" }, { url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" }, { url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" }, { url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" }, { url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" }, { url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" }, { url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" }, { url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" }, { url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" }, { url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" }, { url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" }, { url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" }, { url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" }, { url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" }, { url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" }, { url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" }, { url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" }, { url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" }, { url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" }, { url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" }, { url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" }, { url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" }, { url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" }, { url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" }, { url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" }, { url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" }, { url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" }, { url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" }, { url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" }, ] [[package]] name = "cffi" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } 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" } wheels = [ { 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" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "ezgb" version = "0.2.1" source = { editable = "." } dependencies = [ { name = "pygit2", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pygit2", version = "1.19.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [package.dev-dependencies] dev = [ { name = "mypy" }, { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, { name = "ty" }, { name = "typing-extensions" }, ] [package.metadata] requires-dist = [{ name = "pygit2" }] [package.metadata.requires-dev] dev = [ { name = "mypy" }, { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, { name = "ty" }, { name = "typing-extensions", specifier = ">=4.4" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "librt" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/77/900294b3c9a81415323d81b3f3f271a7f5d7ed9d01e824fc38ffa49470b6/librt-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6", size = 148668, upload-time = "2026-09-29T00:44:20.282Z" }, { url = "https://files.pythonhosted.org/packages/55/3b/c301e8cc47758e4518b93e926a0111080155cc79fa03651d202ac2025065/librt-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978", size = 153562, upload-time = "2026-09-29T00:44:21.672Z" }, { url = "https://files.pythonhosted.org/packages/62/b1/4d5b98114a58bf17e3832cd33400e40f2582f68c2fb7ce25b4468cb1afd3/librt-0.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd", size = 494337, upload-time = "2026-09-29T00:44:23.096Z" }, { url = "https://files.pythonhosted.org/packages/88/36/14223c9b0dc38982eb4f399732c8cb27b19c7e027ae0faef5ca51d08d19f/librt-0.16.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144", size = 485440, upload-time = "2026-09-29T00:44:24.555Z" }, { url = "https://files.pythonhosted.org/packages/ec/9e/d7155e6800d0c33e297e529b217649f7361d706491ebf9d2fc0fc627309e/librt-0.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671", size = 498363, upload-time = "2026-09-29T00:44:26.03Z" }, { url = "https://files.pythonhosted.org/packages/d3/ce/d5f1fb9f913ac0979d6fc5db9fb9c0bf28cc3470988528957922f15ed40c/librt-0.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262", size = 515453, upload-time = "2026-09-29T00:44:27.566Z" }, { url = "https://files.pythonhosted.org/packages/b4/d4/ca0fca1a27c26080d1a25b2b31f271e532078a5719878c3daca0c3d4c539/librt-0.16.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a", size = 509537, upload-time = "2026-09-29T00:44:29.14Z" }, { url = "https://files.pythonhosted.org/packages/ee/c0/26807bf2221a53a2c66f2e23a5c4d57f1c0eaa4afed7cb81d841cf3d1903/librt-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c", size = 532541, upload-time = "2026-09-29T00:44:30.745Z" }, { url = "https://files.pythonhosted.org/packages/0e/50/c329d509907e1bc061abeec3796f94d255d43ba25d28c3854d2138ed5ab2/librt-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726", size = 537059, upload-time = "2026-09-29T00:44:32.102Z" }, { url = "https://files.pythonhosted.org/packages/84/d6/4571b9c9e65d98e5503734166dce2fd3e216f526dc3c59a3773faa8a60e3/librt-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a", size = 536614, upload-time = "2026-09-29T00:44:33.653Z" }, { url = "https://files.pythonhosted.org/packages/c8/d3/a2ed8a292e4ab7889792a9c3f4ad5d1caf6557afce7f6b452e77f8ed20b7/librt-0.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1", size = 517199, upload-time = "2026-09-29T00:44:35.115Z" }, { url = "https://files.pythonhosted.org/packages/9e/24/276e5218b4b7adb6258a42b7403cb1b7cef25616238d0c2152288a9d2f53/librt-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143", size = 558746, upload-time = "2026-09-29T00:44:36.656Z" }, { url = "https://files.pythonhosted.org/packages/7c/5c/9156df81a1726e1efc9567379def2b8777a6d56f464d2f1b760bf951b8af/librt-0.16.0-cp310-cp310-win32.whl", hash = "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba", size = 104424, upload-time = "2026-09-29T00:44:38.123Z" }, { url = "https://files.pythonhosted.org/packages/0b/62/4fd172cb3f49ab2cf5ef17de519a93cfb935202c1270ba9d44c6a64178c3/librt-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155", size = 124267, upload-time = "2026-09-29T00:44:39.455Z" }, { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" }, { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" }, { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" }, { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" }, { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" }, { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" }, { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" }, { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" }, { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" }, { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" }, { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" }, { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" }, { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" }, { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" }, { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" }, { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" }, { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" }, { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" }, { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" }, { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" }, { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" }, { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" }, { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" }, { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" }, { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" }, { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" }, { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" }, { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" }, { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" }, { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" }, { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" }, { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, ] [[package]] name = "mypy" version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0a/78c9ac93933e2ecd51d807021f78c03ffe98640f33951a8be159f5fd55bc/mypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffda5244fd1ad71a1e54405e35f50d09b80c3978efa120f58bd1252ae32c62d2", size = 14408618, upload-time = "2026-10-01T20:39:46.766Z" }, { url = "https://files.pythonhosted.org/packages/aa/4f/5f5f4429fd5a4218ce6268164a66afeed75c7e2ce5522a4d163faaa5fe85/mypy-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:615b03922d40e186fd1df73156473db0ba525c639bb4fd1cf28e1694879c9b23", size = 14552749, upload-time = "2026-10-01T20:40:08.325Z" }, { url = "https://files.pythonhosted.org/packages/54/78/fa1c319cd51db7a6027b46117460d5a361c6bba0c0f0f0670a2dab8ce2a5/mypy-2.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13fa24f439c0e48a290a3922fa14ccd22f2762bae99d2142931b3e40a9055080", size = 15505846, upload-time = "2026-10-01T20:39:15.475Z" }, { url = "https://files.pythonhosted.org/packages/e2/5a/14c02690776fb04e9fa60b81173cb9ae6315e0745fad3813b303737f2f0c/mypy-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3adef556a19eb3b630bf86a79c29d7da3d61e541472d0d897ca01b171c0abf8f", size = 15823059, upload-time = "2026-10-01T20:39:35.429Z" }, { url = "https://files.pythonhosted.org/packages/76/d8/d29afa819f8cb765018d6e2773e390a41ea4592f75abe417135969646857/mypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:1dc0f64b0a92ae27a49d2175f0bacfa56e15bdc5b420cf92e0c1f79292219cbb", size = 11312927, upload-time = "2026-10-01T20:39:28.509Z" }, { url = "https://files.pythonhosted.org/packages/bc/c8/966dec4d80303855d824e6bf67fe1da812d88adfe4d75e24d8545dab4826/mypy-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a378fc15fb33e321f04652c166ce73eeb8833a97c3d218132844e938cd93220", size = 10879252, upload-time = "2026-10-01T20:38:41.545Z" }, { url = "https://files.pythonhosted.org/packages/26/67/e53965e67d23d48e78dd603aa7525a8d77e7818c23954cc217ccde5cd5c0/mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd", size = 14315745, upload-time = "2026-10-01T20:39:25.03Z" }, { url = "https://files.pythonhosted.org/packages/b1/5d/a588ccd57625822d8484d7aff4e9c70f88d042a91c2ec84324a2bc14b3f3/mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74", size = 14465652, upload-time = "2026-10-01T20:38:57.19Z" }, { url = "https://files.pythonhosted.org/packages/05/af/b4978a5566cb829ca563dc08e3ad8d944f116078786ad54ba3dff2bcb64d/mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1", size = 15419180, upload-time = "2026-10-01T20:40:27.808Z" }, { url = "https://files.pythonhosted.org/packages/b2/8c/52c71f31c54d0a749d832cc73b933c6e38c52d9b270d5503e031c73723dc/mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4", size = 15669212, upload-time = "2026-10-01T20:39:20.443Z" }, { url = "https://files.pythonhosted.org/packages/e8/55/ba63121494404fd7d3787ec176ccd02820d6ec125ac4e7c4c0b79f2cc6c7/mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b", size = 11299572, upload-time = "2026-10-01T20:40:03.627Z" }, { url = "https://files.pythonhosted.org/packages/9c/9c/385c81b5c0d3a2917f077c4da660d0493f9521ccfd4b058e33b860f72d02/mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd", size = 10869751, upload-time = "2026-10-01T20:40:25.483Z" }, { url = "https://files.pythonhosted.org/packages/68/ed/e5d7cf4017e74a1c1e1c4058ce8f614fc1e3e7606564f47166e22bfc9f95/mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e", size = 14378593, upload-time = "2026-10-01T20:38:48.222Z" }, { url = "https://files.pythonhosted.org/packages/30/7d/12d994886a922f0f1997becc9c6625198d61eeb5962558a886af7dd38d54/mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90", size = 14464697, upload-time = "2026-10-01T20:39:11.059Z" }, { url = "https://files.pythonhosted.org/packages/f3/9e/bcc9af755425ad17790bf11d73c2ea7592914cf309ee1340997670f9d57a/mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e", size = 15552257, upload-time = "2026-10-01T20:39:56.269Z" }, { url = "https://files.pythonhosted.org/packages/af/0c/3343fc4525d6f00d75ad17a93a9f052d5163641cb8911840d4a12cb59ff2/mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1", size = 15800443, upload-time = "2026-10-01T20:38:54.815Z" }, { url = "https://files.pythonhosted.org/packages/8f/0e/69aac6b8159da7c53e6115a2be1bf48e85503402cadbf506dd708e8b2ad7/mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946", size = 11381574, upload-time = "2026-10-01T20:39:49.406Z" }, { url = "https://files.pythonhosted.org/packages/6e/d3/d32ce4feb5993eec09d2024bef16cedc9b93701b1446b86092f08b24491b/mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989", size = 10944729, upload-time = "2026-10-01T20:40:15.203Z" }, { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" }, { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" }, { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" }, { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" }, { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" }, { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" }, { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" }, { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" }, { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" }, { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" }, { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" }, { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" }, { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" }, { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" }, { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" }, { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" }, { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" }, { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" }, { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" }, { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" }, { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" }, { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" }, { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" }, { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" }, { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" }, { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" }, { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" }, { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" }, { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" }, { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" }, { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" }, { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" }, { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "packaging" version = "26.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "pathspec" version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pygit2" version = "1.18.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/ea/762d00f6f518423cd889e39b12028844cc95f91a6413cf7136e184864821/pygit2-1.18.2.tar.gz", hash = "sha256:eca87e0662c965715b7f13491d5e858df2c0908341dee9bde2bc03268e460f55", size = 797200, upload-time = "2025-08-16T13:52:36.853Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/54/a747b5a80698c22b7e510de61facaf7b7dd196fe4540d0d28eb05eacaeba/pygit2-1.18.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a84fbc62b0d2103059559b5af7e939289a0f3fc7d0a7ad84d822eaa97a6db687", size = 5509510, upload-time = "2025-08-16T13:39:01.887Z" }, { url = "https://files.pythonhosted.org/packages/d4/bc/865c6090efa25a5cfe7e1d2cec28c2515a2d7239d3b428f36184af6610ac/pygit2-1.18.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c84aa50acba5a2c6bb36863fbcc1d772dc00199f9ea41bb5cac73c5fdad42bce", size = 5762592, upload-time = "2025-08-16T13:39:03.06Z" }, { url = "https://files.pythonhosted.org/packages/41/96/69a408e57fd68555e1bdb134a15edb4cb77a24ba266dcbf6edf6d5d4a807/pygit2-1.18.2-cp310-cp310-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d7b8570f0df4f0a854c3d3bdcec4a5767b50b0acb13ef163f6b96db593e3611f", size = 4599930, upload-time = "2025-08-16T13:39:04.66Z" }, { url = "https://files.pythonhosted.org/packages/aa/bc/ee2335c98995cce3dfec7ccd54fff027b769a839832457fa784fe14e4538/pygit2-1.18.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cccceadab2c772a52081eac4680c3664d2ff21966171d339fee6aaf303ccbe23", size = 5493592, upload-time = "2025-08-16T13:39:06.025Z" }, { url = "https://files.pythonhosted.org/packages/31/54/af78c3870c62b3bbfe86ed1f2ee1f46a8a43c1db70c0d35769365fa8b145/pygit2-1.18.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c51e0b4a733e72212c86c8b3890a4c3572b1cae6d381e56b4d53ba3dafbeecf2", size = 5760887, upload-time = "2025-08-21T13:32:22.347Z" }, { url = "https://files.pythonhosted.org/packages/23/de/419658ecdbf37e89094b171b63c941774ff46d1bb6f65efd40f0c25d1df9/pygit2-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:970e9214e9146c893249acb9610fda9220fe048ae76c80fd7f36d0ec3381676b", size = 5460906, upload-time = "2025-08-16T13:39:07.633Z" }, { url = "https://files.pythonhosted.org/packages/c7/91/bbaca03aa624915c4dd95c60961f34d683b069249c0f25d1faef29195873/pygit2-1.18.2-cp310-cp310-win32.whl", hash = "sha256:546f9b8e7bf9d88d77008a82d7d989c624f5756c4fba26af1b8985019985dc8a", size = 1238396, upload-time = "2025-08-16T13:10:33.39Z" }, { url = "https://files.pythonhosted.org/packages/53/a5/1d10b3e9d85ca62cbe5d5bbda611d3ca1f5fd0603910a00132b440bbbfd9/pygit2-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:5383cdfc1315e7d49d7a59a9aa37c4f0f60d08c4de3137f31d20e4be2055ad47", size = 1323973, upload-time = "2025-08-16T13:15:10.479Z" }, { url = "https://files.pythonhosted.org/packages/3e/c5/d3bd32443f4d7275928f7e07beb87b907401570e4a0b2d6b671909373d23/pygit2-1.18.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3fc89da1426793227e06f2dec5f2df98a0c6806fb4024eec6a125fb7a5042bbf", size = 5509503, upload-time = "2025-08-16T13:39:09.095Z" }, { url = "https://files.pythonhosted.org/packages/71/e4/b26e970a493f65f646ec33ab77c462c6cb6b5527a11aa51b0b18bfe47642/pygit2-1.18.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6ab37a87b58032c596c37bcd0e3926cc6071748230f6f0911b7fe398e021ae", size = 5768944, upload-time = "2025-08-16T13:39:10.622Z" }, { url = "https://files.pythonhosted.org/packages/86/32/09d5ef009dd28529afcf377f4a767156fd105b58496405a815e4b66c1944/pygit2-1.18.2-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d9642f57943703de3651906f81b9535cb257b3cbe45ecca8f97cf475f1cb6b5f", size = 4606504, upload-time = "2025-08-16T13:39:12.131Z" }, { url = "https://files.pythonhosted.org/packages/6c/2f/13fddef74a8dd6080e24a0bbd19c253e13e293f52c282596b9e3d0dc9148/pygit2-1.18.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1aa3efba6459e10608900fe26679e3b52ea566761f3e7ef9c0805d69a5548631", size = 5500249, upload-time = "2025-08-16T13:39:13.727Z" }, { url = "https://files.pythonhosted.org/packages/80/c5/235376a6908a4b7cf25f92e3090e4f3f9828af49d021299a89eae66ecf9e/pygit2-1.18.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:25957ccf70e37f3e8020748724a14faf4731ceac69ed00ccbb422f99de0a80cc", size = 5767739, upload-time = "2025-08-21T13:33:47.707Z" }, { url = "https://files.pythonhosted.org/packages/a2/1e/e2f914bfa0e8ca0b7c518c32d1b2183254c21d7d1eca3e21d6aeb7ccf066/pygit2-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6c9cdbad0888d664b80f30efda055c4c5b8fdae22c709bd57b1060daf8bde055", size = 5467750, upload-time = "2025-08-16T13:39:15.414Z" }, { url = "https://files.pythonhosted.org/packages/d0/96/ac263bc9ce48a4f9cc31437dcaa812cc893382a8837c32cfe4764b03127e/pygit2-1.18.2-cp311-cp311-win32.whl", hash = "sha256:91bde9503ad35be55c95251c9a90cfe33cd608042dcc08d3991ed188f41ebec2", size = 1238394, upload-time = "2025-08-16T13:19:37.689Z" }, { url = "https://files.pythonhosted.org/packages/fd/98/7fae3f7779469f2f4514e20d887d4011953c0a996af4b7f6b8bb73de4c0f/pygit2-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:840d01574e164d9d2428d36d9d32d377091ac592a4b1a3aa3452a5342a3f6175", size = 1324157, upload-time = "2025-08-16T13:24:17.196Z" }, { url = "https://files.pythonhosted.org/packages/bd/bf/469ec748d9d7989e5494eb5210f0752be4fb6b6bf892f9608cd2a1154dda/pygit2-1.18.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5eaf2855d78c5ad2a6c2ebf840f8717a8980c93567a91fbc0fc91650747454a4", size = 5504679, upload-time = "2025-08-16T13:39:17.017Z" }, { url = "https://files.pythonhosted.org/packages/40/95/da254224e3d60a0b5992e0fe8dee3cadfd959ee771375eb0ee921f77e636/pygit2-1.18.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee5dd227e4516577d9edc2b476462db9f0428d3cc1ad5de32e184458f25046ee", size = 5769675, upload-time = "2025-08-16T13:39:18.691Z" }, { url = "https://files.pythonhosted.org/packages/b7/cd/722e71b832b9c0d28482e15547d6993868e64e15becee5d172b51d4a6fed/pygit2-1.18.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07e5c39ed67e07dac4eb99bfc33d7ccc105cd7c4e09916751155e7da3e07b6bc", size = 4605744, upload-time = "2025-08-16T13:39:20.153Z" }, { url = "https://files.pythonhosted.org/packages/3b/50/70f38159f6783b54abcd74f47617478618f98a7f68370492777c9db42156/pygit2-1.18.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12ae4ed05b48bb9f08690c3bb9f96a37a193ed44e1a9a993509a6f1711bb22ae", size = 5504072, upload-time = "2025-08-16T13:39:21.834Z" }, { url = "https://files.pythonhosted.org/packages/e9/79/5648354eeefb85782e7b66c28ac27c1d6de51fd71b716fa59956fd7d6e30/pygit2-1.18.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:00919a2eafd975a63025d211e1c1a521bf593f6c822bc61f18c1bc661cbffd42", size = 5768382, upload-time = "2025-08-21T13:36:33.4Z" }, { url = "https://files.pythonhosted.org/packages/aa/e7/a679120119e92dcdbeb8add6655043db3bc7746d469b7dfc744667ebcd33/pygit2-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f96a168bafb99e99b95f59b0090171396ad2fb07713e5505ad3e4c16a41d56a", size = 5472093, upload-time = "2025-08-16T13:39:23.031Z" }, { url = "https://files.pythonhosted.org/packages/7d/54/e8c616a8fe12f80af64cfb9a7cba5f9455ca19c8ce68e5ef1d11d6a61d85/pygit2-1.18.2-cp312-cp312-win32.whl", hash = "sha256:ff1c99f2f342c3a3ec1847182d236088f1eb32bc6c4f93fbb5cb2514ccbe29f3", size = 1239180, upload-time = "2025-08-16T13:28:53.788Z" }, { url = "https://files.pythonhosted.org/packages/c1/02/f4e51309c709f53575ceec53d74917cd2be536751d4d53f345a6b5427ad4/pygit2-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:507b5ea151cb963b77995af0c4fb51333f02f15a05c0b36c33cd3f5518134ceb", size = 1324567, upload-time = "2025-08-16T13:33:51.181Z" }, { url = "https://files.pythonhosted.org/packages/0e/ff/34dc8ce51f2f9ba39a5f2b34b9a5d70563cc93a387accf562c5c36e40d2b/pygit2-1.18.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f65d6114d96cb7a21cc09e8cb0622d0388619adf9cdb5d77d94589a41996b0a8", size = 5504646, upload-time = "2025-08-16T13:39:24.164Z" }, { url = "https://files.pythonhosted.org/packages/fd/b6/7990c465a5a6967df87323a8a90e19e9b393d238497c62d0aabcb98b9d62/pygit2-1.18.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9367df01958f7e538bc3fc665ace55de0d5b72da5b6b5f95c44ae916c39a6f51", size = 5771485, upload-time = "2025-08-16T13:39:25.386Z" }, { url = "https://files.pythonhosted.org/packages/6d/ad/c31064927a11cb39d4860bbf3a1a1bd944d9768e9c8faaa48b670e9359ed/pygit2-1.18.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb2993e44aaafac5bcd801c2926dcf87c3f8939ff1c5fb9fe0549a81acd27a03", size = 4607179, upload-time = "2025-08-16T13:39:27.264Z" }, { url = "https://files.pythonhosted.org/packages/5d/da/29a3c808bfb42ba86e5aca226fad7871b65fc216e18e14190553a879157b/pygit2-1.18.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d5dc116d6054cb4e970160c09440da7ded36acfbc4f06ef8e0d38ac275ee12", size = 5505911, upload-time = "2025-08-16T13:39:28.623Z" }, { url = "https://files.pythonhosted.org/packages/14/ac/c5afc7dd8ec0deb022ec8bbb5c938725438c40531ab9b6ad2b2d37730c59/pygit2-1.18.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b87e7ab87da09145cb45434e6ad0402695ca72ffb764487ecc09d28abef5507", size = 5770236, upload-time = "2025-08-21T13:37:22.808Z" }, { url = "https://files.pythonhosted.org/packages/ac/d1/1c6882900bf6e0d3d5764937acab7c79ffadb452e33230ba8e5e9dc35695/pygit2-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a0aa809fd5572c8b1123270263720e458afc9e2069e8d0c1079feebc930e6813", size = 5474235, upload-time = "2025-08-16T13:39:30.274Z" }, { url = "https://files.pythonhosted.org/packages/b3/be/7d8233ff8c5b39ca3d4309fa35a097999baa755e92303102599680c05604/pygit2-1.18.2-cp313-cp313-win32.whl", hash = "sha256:8c4423b08786d0fcea0c523b82bc5ec52039b01500a3391472786e89cadf1069", size = 1239177, upload-time = "2025-08-16T13:38:39.619Z" }, { url = "https://files.pythonhosted.org/packages/ba/f8/d61973ec64a6a7afabec5d1308794399797b44daaacf7ae1969b0f83ddab/pygit2-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:aeba6398d5c689c90c133e07f698aeb9f9693cfbb5707fccffd18f2d67d37c6d", size = 1324597, upload-time = "2025-08-16T13:43:31.309Z" }, { url = "https://files.pythonhosted.org/packages/17/3f/da4563009011dd5e4427740ca7fe3f1005158bf6c6670727e8e9d6078d8a/pygit2-1.18.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd82d37cf5ce474a74388a04b9fb3c28670f44bc7fe970cabbb477a4d1cb871f", size = 5318756, upload-time = "2025-08-16T13:39:31.435Z" }, { url = "https://files.pythonhosted.org/packages/7f/08/0aae26a1c74aedfe99b6f529011cd6e9f335f7840a0e92aeaa4620bcf117/pygit2-1.18.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:991fe6bcbe914507abfe81be1c96bd5039ec315354e4132efffcb03eb8b363fb", size = 5043500, upload-time = "2025-08-16T13:39:33.006Z" }, { url = "https://files.pythonhosted.org/packages/57/91/f6655a5d171c0a080a7507b8d6855067f4365b326c0d946c6af12a633a80/pygit2-1.18.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d801d272f6331e067bd0d560671311d1ce4bb8f81536675706681ed44cc0d7dc", size = 5317765, upload-time = "2025-08-16T13:39:34.222Z" }, { url = "https://files.pythonhosted.org/packages/5c/c8/288d1a56092b3e01524d03eeff24a85efc4eaa3861c6813e3098cde9ee02/pygit2-1.18.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e1ff2d60420c98e6e25fd188069cddf8fa7b0417db7405ce7677a2f546e6b03", size = 5042134, upload-time = "2025-08-16T13:39:35.871Z" }, ] [[package]] name = "pygit2" version = "1.19.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", "python_full_version >= '3.11' and python_full_version < '3.15'", ] dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/44/415aa93422b4bfc21a6448acb7e16280d5f33a9a3fae38a384e37b046ae4/pygit2-1.19.3.tar.gz", hash = "sha256:a543e6d4ebb43825564935758dc234e770016fed673b84370d46ae9580558831", size = 810489, upload-time = "2026-06-13T08:06:04.982Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/3a/36e8827f45d9daf79ea94cbc6148e6243df1fcabc7cac55cd152c5fa99a8/pygit2-1.19.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59bca67a3f2af724659494bec9fed288b99242be13d76349d0170177512712fa", size = 5710933, upload-time = "2026-06-13T08:04:44.661Z" }, { url = "https://files.pythonhosted.org/packages/38/77/9e87bbe723c50d6841740449c6e80a7aa63da01af59ca84b1169450dea7c/pygit2-1.19.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb2e65666356f072d774f935aa0858f68ea34644d7a1467ea4918470748a2722", size = 5699637, upload-time = "2026-06-13T08:04:46.804Z" }, { url = "https://files.pythonhosted.org/packages/2c/89/d0ba3e7f3cea1ec7c359d0456fc2cc3d5010c2a8a5a26da5298284e32a1c/pygit2-1.19.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0424822de48cb466f54c45c4d73924be201fd8ad347ccf0545fe7e601f056ebb", size = 6035985, upload-time = "2026-06-13T08:04:48.46Z" }, { url = "https://files.pythonhosted.org/packages/29/d0/63e37c75f8822b6d2cb0e1877a5e3de7e29a15e3ed2fe48c37b47164ff71/pygit2-1.19.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6aeb43e12fde33ec981209da90f66076e956538e457e4a50ba1665f97e3b2ea", size = 4639470, upload-time = "2026-06-13T08:04:50.116Z" }, { url = "https://files.pythonhosted.org/packages/52/30/23ad5ac73519086eab4953cd9999eb24d57625826fcd28de2e2b57a9bf62/pygit2-1.19.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eaf59b7f6435c276850d34392503b55e703392094ff4c87c3c0a4161b3d90133", size = 5795950, upload-time = "2026-06-13T08:04:51.696Z" }, { url = "https://files.pythonhosted.org/packages/ec/1d/cad147ad317bc61e549784db8ee89391e868dd3da77174d57c6a960e4241/pygit2-1.19.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e536656457dd306d0ebae2ac442c2c8f8798ea2333095b85391bc3423f4ac1b3", size = 6041627, upload-time = "2026-06-13T08:04:53.232Z" }, { url = "https://files.pythonhosted.org/packages/af/03/bcef734c7c46ab8702ce7f06f30f45716a7d8f97ec45fafdb20baef40473/pygit2-1.19.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38d52f8d30bf969e3bfcab22cf5cdc4dc69590e06e8e948b1bb25cccb3bbbe3d", size = 5766485, upload-time = "2026-06-13T08:04:55.035Z" }, { url = "https://files.pythonhosted.org/packages/42/b6/1f04fab2433f6ad10bd8820c1296f557d2fc9131abaff73d89783d8b85ac/pygit2-1.19.3-cp311-cp311-win32.whl", hash = "sha256:8b7a0a16372b245d065efb10a4ef5d61dc08794a69fa8ed6dd1ff918409b7d84", size = 944332, upload-time = "2026-06-13T08:04:56.358Z" }, { url = "https://files.pythonhosted.org/packages/21/66/a88310e96bfbd47feea5359d2380d90326ec5de3a114772ae7e515f3653d/pygit2-1.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:2b8f748836ac00fd785373ef8f5c9be1ed509e357cb270cc3f5674c0ad44d44d", size = 1254403, upload-time = "2026-06-13T08:04:57.447Z" }, { url = "https://files.pythonhosted.org/packages/97/ed/aa3a0a44c2d4419b1c01804ea1ff232bdbcaaf1a1a7f1f8274ff62d9534e/pygit2-1.19.3-cp311-cp311-win_arm64.whl", hash = "sha256:98f2241ef66ec97ab21a2a1a6851c1305929c7a9e9e3e94db5faeb5d2ed06aa2", size = 969498, upload-time = "2026-06-13T08:04:58.863Z" }, { url = "https://files.pythonhosted.org/packages/a2/9c/9bc9a8d727b2ae8ae77306ecfb77a5dcf836da4997d4f7053c893f8e0d11/pygit2-1.19.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d696ee240cc8b84e747b04443d85768c859ef0b88abe4a3505dc0b8e2a953ca0", size = 5708797, upload-time = "2026-06-13T08:05:00.097Z" }, { url = "https://files.pythonhosted.org/packages/63/7e/9099ea2f90791549185f8ac737a8b448e05cf3882cc79928a380be9bf38c/pygit2-1.19.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86318a704bef836067fa9ac06c56591738d548dcdf2ea24ec227257e3f8115f8", size = 5700295, upload-time = "2026-06-13T08:05:01.676Z" }, { url = "https://files.pythonhosted.org/packages/39/84/e9610f041dc43699fca8af55050f9bf9fe0d340ab9902ab8e3fa67bece23/pygit2-1.19.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc3ddb3e5d16cf662d2d0faeb53259e31ae584cc990cd76df909beeb4a36736a", size = 6036822, upload-time = "2026-06-13T08:05:03.473Z" }, { url = "https://files.pythonhosted.org/packages/64/bd/419ec17df3f1f1182de8c8fe800b698ff2a489cc50ec2d34990092bf3d61/pygit2-1.19.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73820fb34339fe3c52731d1c46c298a3239ee75b61bc3a2d9b45f8b8873acc47", size = 4638181, upload-time = "2026-06-13T08:05:05.145Z" }, { url = "https://files.pythonhosted.org/packages/90/5f/aecca9c7f4ffdc1facc4b2402ea40073848e67c8c607a5f0698e1e8a61e4/pygit2-1.19.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20f6ad50608f51b053bf6c321ae2c31cc57f15e4131a4609261c44e29bdae7cb", size = 5799910, upload-time = "2026-06-13T08:05:06.48Z" }, { url = "https://files.pythonhosted.org/packages/ea/2a/e131af7752f75f70e04f7f3ae724481d0e2db9c6c688a2081ffb5948657e/pygit2-1.19.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2c63c7a371dbd5825e5fb7069b759c79aa5dec97806d49cb2c9f9a8a373209d", size = 6042766, upload-time = "2026-06-13T08:05:08.052Z" }, { url = "https://files.pythonhosted.org/packages/6f/a5/978ee5233379b2aa725c290a7cc3baca9b141f4fc37d55d9a473631b529b/pygit2-1.19.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:977eb52cd4134ff60d3faa168327a03f8724278c38bfcd347d8e3a11041ee9cd", size = 5771434, upload-time = "2026-06-13T08:05:09.398Z" }, { url = "https://files.pythonhosted.org/packages/26/33/7c506492fffe3e92d9e911fae43a4b4a3dee43682b71b7793d586c25acfc/pygit2-1.19.3-cp312-cp312-win32.whl", hash = "sha256:14a2734d793d2d937d3bfc9d35b280ec83fc97a91b1934b1013554d41e6d3d70", size = 945030, upload-time = "2026-06-13T08:05:11.187Z" }, { url = "https://files.pythonhosted.org/packages/82/ff/57ad08d1e87ae6e4208f80923ce1a6ea8a03ea776376e39d64816637b168/pygit2-1.19.3-cp312-cp312-win_amd64.whl", hash = "sha256:a7caeaf46aaa8e51c512af9ac1377f388e4836c9d042e134bbc5e22519fbd1bf", size = 1254825, upload-time = "2026-06-13T08:05:12.315Z" }, { url = "https://files.pythonhosted.org/packages/2d/cf/476c217a4ab3c9b4abf2506fb1135e54b19d205ce7ddecb1d961bf93a9dc/pygit2-1.19.3-cp312-cp312-win_arm64.whl", hash = "sha256:2aafa010e3ac227913f398c8a680bce419e396dfe651685fffb779ed1c109a00", size = 969650, upload-time = "2026-06-13T08:05:13.438Z" }, { url = "https://files.pythonhosted.org/packages/7f/12/1e0f0fc54a24ddfdabb0da0bafb3237c82b5628a8c5258ba66fb8d39b5b2/pygit2-1.19.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9238bf7a8a953f4faedcbe90eb097b760a2709c7e7ab38edcf18dc2d0ce9dcff", size = 5708790, upload-time = "2026-06-13T08:05:14.755Z" }, { url = "https://files.pythonhosted.org/packages/82/12/9093b7b81de7fe869f6ae51319a05b9f0a8b2567674e0d61fd54aacb833f/pygit2-1.19.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f999a4995504a6c14443de9481b7c6c54802663fe80c7504e1b946348f43786", size = 5700256, upload-time = "2026-06-13T08:05:16.363Z" }, { url = "https://files.pythonhosted.org/packages/eb/07/a5133f79cf86e8a0cadf9f424cdd5b912ba47410c7254c0237b6e5290ddb/pygit2-1.19.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0918bc9b7597f243aa4b9c785912ae853e9aee4f572bcd5c9c591fcb703542f7", size = 6037556, upload-time = "2026-06-13T08:05:17.823Z" }, { url = "https://files.pythonhosted.org/packages/0e/fc/3aa049d88eaa51f14f1fbfc0b70ac4c2362d3306dbeb8d1e5e3ca301e1dc/pygit2-1.19.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18403fee0171867be5fc1c1415bcd5d5a7fab51299bdd44bcf467c22a868fd22", size = 4638864, upload-time = "2026-06-13T08:05:19.325Z" }, { url = "https://files.pythonhosted.org/packages/bf/b7/5e772367db14f800048fa2a0981abd45baae6dcd8b83453da6ac0f92cead/pygit2-1.19.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cacece712337029092a98cf1af8d244e92fa8b36e5a64d7e14212ff73ea821e8", size = 5801146, upload-time = "2026-06-13T08:05:20.748Z" }, { url = "https://files.pythonhosted.org/packages/07/79/075b37b9a971cc7df9721add0e03cded83cab9e7d9736a80820357b2a972/pygit2-1.19.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49d7c494bdaa5a8da366e4c4b7c822e52bd33bb6f22c67f8585b5396e91f8ac7", size = 6043951, upload-time = "2026-06-13T08:05:22.232Z" }, { url = "https://files.pythonhosted.org/packages/94/76/72e13c487d013f21f97690f0662cb03533d91aad39e0c7c4c8cd3e4453d0/pygit2-1.19.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2bbe74fbe553c6f991512391822c27c9321e3a0b45cab872f45c8d458393eee3", size = 5772767, upload-time = "2026-06-13T08:05:23.487Z" }, { url = "https://files.pythonhosted.org/packages/a1/24/5ba37f80fcb303bcf1a3813631e5b5dddfbac771cce7efc0b03428f4fa2e/pygit2-1.19.3-cp313-cp313-win32.whl", hash = "sha256:3ab184751ca7a92007dbd8ae4ae4ac3b7228ef60ae2d2e18129b44bbe5c0aa4c", size = 945026, upload-time = "2026-06-13T08:05:25.053Z" }, { url = "https://files.pythonhosted.org/packages/ba/48/4ae19f2e4dc8ff2ddc2fd64de7fd1d26aeb2b453069d6c9ee251dd567983/pygit2-1.19.3-cp313-cp313-win_amd64.whl", hash = "sha256:522d9c6ab9206880dceaae5516720c0f0df706859d2ba97f49adcaeea2d661bd", size = 1254793, upload-time = "2026-06-13T08:05:26.174Z" }, { url = "https://files.pythonhosted.org/packages/7a/7b/473ca9e35dfd82c758b700a6cf4ecc107b849674e91ab219383063ebb12f/pygit2-1.19.3-cp313-cp313-win_arm64.whl", hash = "sha256:c77d5a1334ca3f1432c83457e03be8759d8e9040e0d25c59c738f2318545654a", size = 969666, upload-time = "2026-06-13T08:05:27.351Z" }, { url = "https://files.pythonhosted.org/packages/99/15/ec8a777b497853edab4e150c9ea46019b6c709b3c2b06fd06454a1aaa8c6/pygit2-1.19.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:214140f2201474e9d8e3889e5635e323aa90a58e6ae224a9de56be83d3e9b747", size = 5710849, upload-time = "2026-06-13T08:05:28.71Z" }, { url = "https://files.pythonhosted.org/packages/fa/be/a8c449fba4c70239d6e9d0023b1fdd783c2dbb52872fef9c4de1652e930a/pygit2-1.19.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:46fc9fd234b2a38176625af6bfd2dbad99ab937379a980761c59a343bdff4910", size = 5700188, upload-time = "2026-06-13T08:05:30.466Z" }, { url = "https://files.pythonhosted.org/packages/b3/19/8e92b4008324137878e9f9e130b4e349956d2a2b719521b8ce45aad6a0de/pygit2-1.19.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb9f46b561ee53f22cb479e7fdefd7b8ae478d12656436a48166c2ad81a3c021", size = 6039510, upload-time = "2026-06-13T08:05:31.892Z" }, { url = "https://files.pythonhosted.org/packages/90/8f/baf3a0944f54acc27ceaa22b47ca52cd64c8bb1ec69473f80d3f4bafe54d/pygit2-1.19.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f393932fadd8e74f26d7ebb58da649e7a188d9156b732dd6c9f0dfc273d4430", size = 4643029, upload-time = "2026-06-13T08:05:33.385Z" }, { url = "https://files.pythonhosted.org/packages/69/1b/0b045e74fe01b29af28a5d54a9c89693db97e7e6cbd700f8086f4294e85e/pygit2-1.19.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9521a9b45de3affd82618e94f7e5116685e695e65fcbf67b2f1e44a086c0f4a", size = 5803099, upload-time = "2026-06-13T08:05:34.873Z" }, { url = "https://files.pythonhosted.org/packages/4c/d2/0d4eb93b7088316b9885b9c29b6a971009a20bf93aebd5762d3b809d72bf/pygit2-1.19.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:241f0fcd2aaf025e9fb5e1887eac5f244cb6b0a6e83dc27d673fa634ab0e0ee3", size = 6044603, upload-time = "2026-06-13T08:05:36.191Z" }, { url = "https://files.pythonhosted.org/packages/2c/8a/5a486a8aa9a1aa6a1ed5189b71b1b96e92bd164cc3e4d010e285032a2662/pygit2-1.19.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1729caee68cdf35c0cdb913de3850724438e4bdf480742da4f21a3e86d83662", size = 5773603, upload-time = "2026-06-13T08:05:37.605Z" }, { url = "https://files.pythonhosted.org/packages/94/ff/e6a5da1fe3b104fc153160bfe56a85298a6c81b5411ee8de97e4039d3a2f/pygit2-1.19.3-cp314-cp314-win32.whl", hash = "sha256:2c300d234ef15e4c557a7e9ac880163606b364d0d4e20ddbf1204b4ee9f9b61c", size = 965699, upload-time = "2026-06-13T08:05:39.151Z" }, { url = "https://files.pythonhosted.org/packages/bc/a1/a8f21afe574f22fde3d04fb102c4e75b5a1077fa1ea1ee3f7b072aa8f858/pygit2-1.19.3-cp314-cp314-win_amd64.whl", hash = "sha256:0ff9f187b01d6629c14ad069b100e147093b70279496984faa012783c5832b66", size = 1288277, upload-time = "2026-06-13T08:05:40.315Z" }, { url = "https://files.pythonhosted.org/packages/95/65/f5268ac09c91f25e349a658cd9722b3528b1c4d6b18aab7472132e067912/pygit2-1.19.3-cp314-cp314-win_arm64.whl", hash = "sha256:cdfbf879ee0d468d7929eb26e35db7442b56a411af1d5b7d28e858cccf71598a", size = 997247, upload-time = "2026-06-13T08:05:41.629Z" }, { url = "https://files.pythonhosted.org/packages/e9/98/9825e08b78a58bc99e979e322b8c776e6a4b49dcebf68471b650bf97e1ed/pygit2-1.19.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79a3b565e20bbbbf21d29de1675a18e0e556545a2c738f38c0ac0435269f56bd", size = 5714170, upload-time = "2026-06-13T08:05:43.326Z" }, { url = "https://files.pythonhosted.org/packages/82/00/8836f806512ac0a9e8613b2b446a0fe2578834c01c2234ca6d6f2abd6751/pygit2-1.19.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:828856512070cda223c19046b02af50135a5b0efa274a80fbf7c82d773f8e89f", size = 5703231, upload-time = "2026-06-13T08:05:44.997Z" }, { url = "https://files.pythonhosted.org/packages/1f/9b/117fcbd26d30729e9f46889747823e51cb5597d191c100da298faa79bb90/pygit2-1.19.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eec2b1d50bf52d2139d3d478d6d5f5ee56103da4a6dbd786344e8a85254dd07b", size = 6097836, upload-time = "2026-06-13T08:05:46.269Z" }, { url = "https://files.pythonhosted.org/packages/70/8c/c1fa28b4e3064ef0e06e49af3cbcbf50b62f36fab3f9832c15cd46d0a175/pygit2-1.19.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbfa10f22670e7fef13bf249c32aebcafe23115790155d6b71efafb9aa7d4cba", size = 4697771, upload-time = "2026-06-13T08:05:47.629Z" }, { url = "https://files.pythonhosted.org/packages/2b/c5/1d72a285fe794e2ce87ebe4b107c7cc713917e6e62b24264580b966740ea/pygit2-1.19.3-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d22c2940bde27f9ffe095aac87e7c8b8a9c787f91782c7463387d39a2592d04c", size = 5854885, upload-time = "2026-06-13T08:05:48.988Z" }, { url = "https://files.pythonhosted.org/packages/60/ac/250da1cff67e38af607792ed98a49c64b5980d5332d5f26c1882f722cefe/pygit2-1.19.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0ed9f15b4b0b80191669fdf5b702b697ffbc245d24995befa80e46bef3aea25e", size = 6100802, upload-time = "2026-06-13T08:05:50.395Z" }, { url = "https://files.pythonhosted.org/packages/33/fc/1208276e01868b7ec1eb4c108dedabee114e24809eecf08820f9a52c3435/pygit2-1.19.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:40f1253e042d0a5dfed16eec92b5b28a529c2873f4ee77ca60d1ea7b79a14600", size = 5824124, upload-time = "2026-06-13T08:05:51.787Z" }, { url = "https://files.pythonhosted.org/packages/03/3a/04d44e2ff972a8a7846d48ae288f717a058331d6e056a652cafbcd7f6867/pygit2-1.19.3-cp314-cp314t-win32.whl", hash = "sha256:245d627a8ae3f35dafd9ce5b35538ddb5ba0c3fe6be8d2f9aa410f93a9184b83", size = 968784, upload-time = "2026-06-13T08:05:53.107Z" }, { url = "https://files.pythonhosted.org/packages/40/89/d42e745838fa0d9072bf425adf5b590b521b7a196d3b92f40951af9e4514/pygit2-1.19.3-cp314-cp314t-win_amd64.whl", hash = "sha256:8d2e3044ac1b09ef9120deb1896a56423c6dd17eb5ac4012c3c49dd590ee4910", size = 1291338, upload-time = "2026-06-13T08:05:54.546Z" }, { url = "https://files.pythonhosted.org/packages/cf/10/61513d337a9241e9ce54edfd8fc1d8f2b0f36cbfa37de227ded0ffdbdc96/pygit2-1.19.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ef2790c9ff7e7673d7262e9e059c5af86c09197037073fd004d22a63e4151291", size = 998330, upload-time = "2026-06-13T08:05:55.888Z" }, { url = "https://files.pythonhosted.org/packages/fe/52/7645ca57a4987f202487baf86a1d8feaa8c4b45e9bed53415074f17cee94/pygit2-1.19.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a1fe33a036aa2172934cafbdd17e2a0f3a73477727fc147e94f881bfe3961c2", size = 5653015, upload-time = "2026-06-13T08:05:57.131Z" }, { url = "https://files.pythonhosted.org/packages/e1/89/4be52d0e6e58c401b18d54603530fbf0f6988f63081f39b62fffca3cf54f/pygit2-1.19.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:09db53b92c546e621218fb82a97149dc7a155a8323a3dbce6b5048da70b0c00c", size = 5650631, upload-time = "2026-06-13T08:05:58.632Z" }, { url = "https://files.pythonhosted.org/packages/ab/fb/bf14acbc711b40d1a1517c429d8c2a6b0cb6e8513bf1b25f13c79dbb04fa/pygit2-1.19.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c14d61de0c803188a0481d394a8ba61c7e1c38210a1e525097f18a0ad563009e", size = 5563807, upload-time = "2026-06-13T08:06:00.232Z" }, { url = "https://files.pythonhosted.org/packages/45/b8/826cdd7f00bfcf52ae46c3ad405c8279c5431fb91b4171021366efec6a40/pygit2-1.19.3-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:806d1e22d3828ea6767fb025f355374d6dd562161a0312f995723ab15e1de42d", size = 5316588, upload-time = "2026-06-13T08:06:02.341Z" }, { url = "https://files.pythonhosted.org/packages/62/47/9a5d33c552d52e554efe7a39b5022b5455709d75093089ac867750379df0/pygit2-1.19.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5af3422728f61958b6a0faaf7de754ecae5bed689adc31d1b727da3c697a752b", size = 1224962, upload-time = "2026-06-13T08:06:03.897Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyright" version = "1.1.414" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, ] [[package]] name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "ruff" version = "0.16.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, ] [[package]] name = "tomli" version = "2.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "ty" version = "0.0.84" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" }, { url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" }, { url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" }, { url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" }, { url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" }, { url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" }, { url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" }, { url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" }, { url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" }, { url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" }, { url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" }, { url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" }, { url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" }, { url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" }, { url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" }, { url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" }, { url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" }, ] [[package]] name = "typing-extensions" version = "4.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ]