From 877b4dc67a8311d00beffbc5a5e7093bef35acb9 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Wed, 15 Jul 2026 00:19:12 +0800 Subject: [PATCH 1/4] chore(release): prepare HSR packages for PyPI --- .github/workflows/ci.yml | 21 +- .github/workflows/publish.yml | 106 ++- .gitignore | 1 - LICENSE | 661 +++++++++++++++ README.md | 15 +- docs/release.md | 52 +- packages/automas_hsr_adapter_m7a/LICENSE | 661 +++++++++++++++ packages/automas_hsr_adapter_m7a/README.md | 5 +- .../automas_hsr_adapter_m7a/pyproject.toml | 7 +- packages/automas_hsr_adapter_sra/LICENSE | 661 +++++++++++++++ packages/automas_hsr_adapter_sra/README.md | 3 + .../automas_hsr_adapter_sra/pyproject.toml | 7 +- packages/automas_script_hsr/LICENSE | 661 +++++++++++++++ packages/automas_script_hsr/README.md | 3 + packages/automas_script_hsr/pyproject.toml | 9 +- .../src/automas_script_hsr/registry.py | 11 +- .../automas_script_hsr/runtime/autoproxy.py | 2 +- .../src/automas_script_hsr/runtime/history.py | 18 + .../src/automas_script_hsr/runtime/manager.py | 7 +- .../src/automas_script_hsr/runtime/models.py | 2 +- .../runtime/stage_contract.py | 2 +- .../src/automas_script_hsr/schema.py | 2 +- pyproject.toml | 2 + scripts/build_all.py | 3 + scripts/release.py | 134 +++ scripts/smoke_wheels.py | 298 +++++++ tests/test_history.py | 34 + tests/test_package_metadata.py | 54 +- tests/test_registry_contract.py | 19 + tests/test_release.py | 47 ++ tests/test_stage_contract.py | 83 ++ tests/test_wheel_smoke.py | 78 ++ uv.lock | 785 ++++++++++++++++++ 33 files changed, 4374 insertions(+), 80 deletions(-) create mode 100644 LICENSE create mode 100644 packages/automas_hsr_adapter_m7a/LICENSE create mode 100644 packages/automas_hsr_adapter_sra/LICENSE create mode 100644 packages/automas_script_hsr/LICENSE create mode 100644 packages/automas_script_hsr/src/automas_script_hsr/runtime/history.py create mode 100644 scripts/release.py create mode 100644 scripts/smoke_wheels.py create mode 100644 tests/test_history.py create mode 100644 tests/test_registry_contract.py create mode 100644 tests/test_release.py create mode 100644 tests/test_stage_contract.py create mode 100644 tests/test_wheel_smoke.py create mode 100644 uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d04b43c..f99be49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,10 @@ on: permissions: contents: read +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: windows-latest @@ -16,19 +20,28 @@ jobs: - uses: astral-sh/setup-uv@v6 with: python-version: "3.12" - - run: uv sync --all-packages --group dev + enable-cache: true + - run: uv sync --locked --all-packages --group dev - run: uv run python -m unittest discover -s tests -v build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: astral-sh/setup-uv@v6 with: python-version: "3.12" - - run: python -m pip install --upgrade build - - run: python scripts/build_all.py + enable-cache: true + - run: uv sync --locked --all-packages --group dev + - run: uv run python scripts/build_all.py + - run: uv run twine check dist/*/* + - name: Resolve adapters against the locally built core wheel + run: >- + uv run python scripts/smoke_wheels.py dist + --mode local-adapter-resolution - uses: actions/upload-artifact@v4 with: name: distributions path: dist/**/* + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 83730d7..9e69da9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,61 +11,89 @@ on: - automas-script-hsr - automas-hsr-adapter-sra - automas-hsr-adapter-m7a + version: + description: Exact version from the selected package pyproject.toml + required: true + type: string + push: + tags: + - "automas-script-hsr-v*" + - "automas-hsr-adapter-sra-v*" + - "automas-hsr-adapter-m7a-v*" + +permissions: + contents: read + +concurrency: + group: publish-${{ github.ref }}-${{ inputs.package || 'tag' }} + cancel-in-progress: false jobs: - publish-script-hsr: - name: Publish automas-script-hsr - if: inputs.package == 'automas-script-hsr' + build: runs-on: ubuntu-latest - environment: pypi-script-hsr - permissions: - contents: read - id-token: write + outputs: + package: ${{ steps.release.outputs.package }} + environment: ${{ steps.release.outputs.environment }} + version: ${{ steps.release.outputs.version }} + artifact_name: ${{ steps.release.outputs.artifact_name }} steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 with: - python-version: "3.12" - - run: python -m pip install --upgrade build - - run: python -m build packages/automas_script_hsr --outdir dist - - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist - - publish-adapter-sra: - name: Publish automas-hsr-adapter-sra - if: inputs.package == 'automas-hsr-adapter-sra' - runs-on: ubuntu-latest - environment: pypi-adapter-sra - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + fetch-depth: 0 + - uses: astral-sh/setup-uv@v6 with: python-version: "3.12" - - run: python -m pip install --upgrade build - - run: python -m build packages/automas_hsr_adapter_sra --outdir dist - - uses: pypa/gh-action-pypi-publish@release/v1 + enable-cache: true + - run: uv sync --locked --all-packages --group dev + - id: release + name: Resolve and validate release target + env: + EVENT_NAME: ${{ github.event_name }} + RELEASE_REF: ${{ github.ref }} + INPUT_PACKAGE: ${{ inputs.package }} + INPUT_VERSION: ${{ inputs.version }} + run: >- + uv run python scripts/release.py + --event-name "$EVENT_NAME" + --ref "$RELEASE_REF" + --package "$INPUT_PACKAGE" + --version "$INPUT_VERSION" + - name: Verify tag commit belongs to main + if: github.event_name == 'push' + run: git merge-base --is-ancestor "$GITHUB_SHA" origin/main + - run: uv run python -m unittest discover -s tests -v + - name: Build selected distribution + env: + PACKAGE_DIR: ${{ steps.release.outputs.package_dir }} + run: uv run python -m build "$PACKAGE_DIR" --outdir dist + - run: uv run twine check dist/* + - name: Metadata-only smoke test for the selected wheel + env: + PACKAGE: ${{ steps.release.outputs.package }} + run: >- + uv run python scripts/smoke_wheels.py dist + --mode metadata-only + --expected-package "$PACKAGE" + - uses: actions/upload-artifact@v4 with: - packages-dir: dist + name: ${{ steps.release.outputs.artifact_name }} + path: dist/* + if-no-files-found: error + retention-days: 7 - publish-adapter-m7a: - name: Publish automas-hsr-adapter-m7a - if: inputs.package == 'automas-hsr-adapter-m7a' + publish: + needs: build runs-on: ubuntu-latest - environment: pypi-adapter-m7a + environment: + name: ${{ needs.build.outputs.environment }} permissions: contents: read id-token: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/download-artifact@v4 with: - python-version: "3.12" - - run: python -m pip install --upgrade build - - run: python -m build packages/automas_hsr_adapter_m7a --outdir dist + name: ${{ needs.build.outputs.artifact_name }} + path: dist - uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist diff --git a/.gitignore b/.gitignore index 83595d0..48f60a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .venv/ -uv.lock dist/ **/build/ **/dist/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 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 GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + 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 +them 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 Affero General Public License as published by + the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index cc44105..11eb005 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # AUTO-MAS HSR Plugins -AUTO-MAS 的星穹铁道插件工作区。仓库统一维护 HSR 调度、StarRailAssistant 和三月七助手的适配代码;每个目录仍独立构建并发布为一个 Python distribution。 +AUTO-MAS 的崩坏:星穹铁道插件工作区,也是 HSR 插件及其引擎适配器的唯一源码仓库。 + +主程序不内置本仓库源码,也不把 HSR 脚本随主程序捆绑安装。用户只需在 AUTO-MAS +插件市场选择 SRA 或三月七助手适配器;适配器依赖会自动安装 HSR 核心包。 ## Packages @@ -8,14 +11,20 @@ AUTO-MAS 的星穹铁道插件工作区。仓库统一维护 HSR 调度、StarRa - `automas-hsr-adapter-sra`:StarRailAssistant 适配器。 - `automas-hsr-adapter-m7a`:三月七助手适配器。 +每个目录独立构建为一个 Python distribution。适配器依赖 +`automas-script-hsr`,适配器之间互不依赖。 + ## Development ```powershell -uv sync --all-packages --group dev +uv sync --locked --all-packages --group dev uv run python -m unittest discover -s tests -v uv run python scripts/build_all.py +uv run twine check dist/*/* +uv run python scripts/smoke_wheels.py dist --mode local-adapter-resolution ``` ## Publishing -通过 GitHub Actions 的 `Publish` 手动工作流发布单个 distribution。发布使用 PyPI Trusted Publishing,不保存长期 PyPI token。 +发行只通过 GitHub Actions 和 PyPI Trusted Publishing 完成,不保存长期 PyPI +token,也不从开发机直接上传。完整配置与首发流程见 [docs/release.md](docs/release.md)。 diff --git a/docs/release.md b/docs/release.md index 5774269..b0d2f67 100644 --- a/docs/release.md +++ b/docs/release.md @@ -6,17 +6,24 @@ - `automas-hsr-adapter-sra` - `automas-hsr-adapter-m7a` -发布顺序必须先核心包,再发布两个适配器。 +首次发布必须先发布核心包,再发布两个适配器。AUTO-MAS 主程序不内置这些源码; +用户只在插件市场安装自己选择的 SRA 或三月七助手适配器,适配器依赖自动拉取核心包。 -## GitHub 设置 +## GitHub Environments -1. 在 `AUTO-MAS-Project` 组织创建 `automas-hsr` 仓库,并将本地仓库推送到 `main`。 -2. 创建三个 GitHub Environments:`pypi-script-hsr`、`pypi-adapter-sra`、`pypi-adapter-m7a`。 -3. 为每个环境设置 Required reviewers,至少包含发布维护者。不要在仓库或环境中保存长期 PyPI token。 +在 `AUTO-MAS-Project/automas-hsr` 创建三个 environment: + +- `pypi-script-hsr` +- `pypi-adapter-sra` +- `pypi-adapter-m7a` + +每个 environment 应配置 required reviewers,并限制部署来源:允许 `main` 用于首次 +手动发布,允许对应的包版本 tag 用于后续自动发布。仓库和 environment 都不保存 +PyPI 用户名、密码或长期 token。 ## PyPI Trusted Publishing -对三个项目分别添加 pending publisher。每条配置均使用 GitHub Actions: +在 PyPI 的 Publishing 页面为三个项目分别创建 pending publisher: | PyPI 项目 | Owner | Repository | Workflow | Environment | | --- | --- | --- | --- | --- | @@ -24,16 +31,37 @@ | `automas-hsr-adapter-sra` | `AUTO-MAS-Project` | `automas-hsr` | `publish.yml` | `pypi-adapter-sra` | | `automas-hsr-adapter-m7a` | `AUTO-MAS-Project` | `automas-hsr` | `publish.yml` | `pypi-adapter-m7a` | -PyPI 的 pending publisher 能在首次 OIDC 发布时创建项目;配置与工作流环境名必须完全一致。 +Pending publisher 会在首次 OIDC 发布时创建对应 PyPI 项目。项目名、workflow 文件名和 +environment 名必须与表格完全一致。 官方说明: - - -## 发布步骤 +## 首次发布 + +1. 确认 `main` 上的 CI 全部通过,且三个 `pyproject.toml` 的版本正确。 +2. 在 GitHub Actions 手动运行 `Publish`,ref 必须选择 `main`。 +3. 选择包并填写与该包 `pyproject.toml` 完全一致的版本号。 +4. 先发布 `automas-script-hsr`;发布成功后再依次发布两个适配器。 +5. 每次都在对应 environment 审批后,才会通过 OIDC 上传。 + +全仓 CI 会分别安装 SRA/M7A adapter,并用 `--no-index` 从本次构建产物解析和安装 +`automas-script-hsr`,验证 adapter 的依赖声明确实会自动拉取 core。发布工作流会重新 +运行测试,只构建一次所选包,并执行 `twine check` 与不解析依赖的单包元数据冒烟测试; +core 按发布顺序已先存在于 PyPI。随后,同一份已验证 artifact 才交给发布 job。手动 +发布若不在 `main`,或填写版本与项目版本不一致,会在获取 PyPI OIDC 权限前失败。 + +## 后续 tag 发布 + +后续版本使用包级 tag,tag 中版本必须与对应 `pyproject.toml` 一致: + +```text +automas-script-hsr-v0.1.1 +automas-hsr-adapter-sra-v0.1.1 +automas-hsr-adapter-m7a-v0.1.1 +``` -1. 合并通过 CI 的版本变更。 -2. 从 GitHub Actions 手动运行 `Publish`,先选择 `automas-script-hsr`。 -3. 发布成功后依次运行两个适配器包。 -4. 在干净环境安装并检查 entry points;随后更新 AUTO-MAS 主仓的插件 bootstrap 版本并进行集成测试。 +tag 必须指向 `main` 历史中的提交。推送后会触发同一套验证和 environment 审批。 +不要复用已发布版本号;PyPI 发行文件不可覆盖。 diff --git a/packages/automas_hsr_adapter_m7a/LICENSE b/packages/automas_hsr_adapter_m7a/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/packages/automas_hsr_adapter_m7a/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 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 GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + 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 +them 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 Affero General Public License as published by + the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/automas_hsr_adapter_m7a/README.md b/packages/automas_hsr_adapter_m7a/README.md index f8370a9..1a9afdc 100644 --- a/packages/automas_hsr_adapter_m7a/README.md +++ b/packages/automas_hsr_adapter_m7a/README.md @@ -1,3 +1,6 @@ # automas-hsr-adapter-m7a -AUTO-MAS HSR 的 March7thAssistant 任务目录与控制器。 +AUTO-MAS HSR 的三月七助手任务目录与控制器。 + +该包通常由 AUTO-MAS 插件管理器按用户选择安装,并依赖 +`automas-script-hsr`;它不随主程序捆绑。 diff --git a/packages/automas_hsr_adapter_m7a/pyproject.toml b/packages/automas_hsr_adapter_m7a/pyproject.toml index 188afe5..3a55197 100644 --- a/packages/automas_hsr_adapter_m7a/pyproject.toml +++ b/packages/automas_hsr_adapter_m7a/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=68", "wheel"] +requires = ["setuptools>=77", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,6 +8,9 @@ version = "0.1.0" description = "March7thAssistant adapter for AUTO-MAS HSR" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.12" +authors = [{ name = "AUTO-MAS Team" }] +license = "AGPL-3.0-or-later" +license-files = ["LICENSE"] dependencies = [ "automas-script-hsr>=0.1.0,<0.2.0", "PyYAML>=6", @@ -16,6 +19,8 @@ dependencies = [ [project.urls] Homepage = "https://github.com/AUTO-MAS-Project/automas-hsr" Repository = "https://github.com/AUTO-MAS-Project/automas-hsr" +Documentation = "https://doc.auto-mas.top/" +Issues = "https://github.com/AUTO-MAS-Project/automas-hsr/issues" [project.entry-points."auto_mas.plugins"] automas_hsr_adapter_m7a = "automas_hsr_adapter_m7a.plugin:Plugin" diff --git a/packages/automas_hsr_adapter_sra/LICENSE b/packages/automas_hsr_adapter_sra/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/packages/automas_hsr_adapter_sra/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 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 GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + 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 +them 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 Affero General Public License as published by + the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/automas_hsr_adapter_sra/README.md b/packages/automas_hsr_adapter_sra/README.md index f98e6a8..6aef47f 100644 --- a/packages/automas_hsr_adapter_sra/README.md +++ b/packages/automas_hsr_adapter_sra/README.md @@ -1,3 +1,6 @@ # automas-hsr-adapter-sra AUTO-MAS HSR 的 StarRailAssistant 任务目录与控制器。 + +该包通常由 AUTO-MAS 插件管理器按用户选择安装,并依赖 +`automas-script-hsr`;它不随主程序捆绑。 diff --git a/packages/automas_hsr_adapter_sra/pyproject.toml b/packages/automas_hsr_adapter_sra/pyproject.toml index 2c77580..ed52027 100644 --- a/packages/automas_hsr_adapter_sra/pyproject.toml +++ b/packages/automas_hsr_adapter_sra/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=68", "wheel"] +requires = ["setuptools>=77", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,11 +8,16 @@ version = "0.1.0" description = "StarRailAssistant adapter for AUTO-MAS HSR" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.12" +authors = [{ name = "AUTO-MAS Team" }] +license = "AGPL-3.0-or-later" +license-files = ["LICENSE"] dependencies = ["automas-script-hsr>=0.1.0,<0.2.0"] [project.urls] Homepage = "https://github.com/AUTO-MAS-Project/automas-hsr" Repository = "https://github.com/AUTO-MAS-Project/automas-hsr" +Documentation = "https://doc.auto-mas.top/" +Issues = "https://github.com/AUTO-MAS-Project/automas-hsr/issues" [project.entry-points."auto_mas.plugins"] automas_hsr_adapter_sra = "automas_hsr_adapter_sra.plugin:Plugin" diff --git a/packages/automas_script_hsr/LICENSE b/packages/automas_script_hsr/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/packages/automas_script_hsr/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 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 GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + 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 +them 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 Affero General Public License as published by + the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/automas_script_hsr/README.md b/packages/automas_script_hsr/README.md index 1b82a3e..bf7cbcc 100644 --- a/packages/automas_script_hsr/README.md +++ b/packages/automas_script_hsr/README.md @@ -1,3 +1,6 @@ # automas-script-hsr AUTO-MAS 的 HSR 统一脚本入口、能力聚合与适配器服务契约。 + +该包由 SRA 或三月七助手适配器作为依赖自动安装,不作为需要用户单独选择的插件, +也不随主程序捆绑。两个适配器均依赖此包。 diff --git a/packages/automas_script_hsr/pyproject.toml b/packages/automas_script_hsr/pyproject.toml index 96f723d..4a2012d 100644 --- a/packages/automas_script_hsr/pyproject.toml +++ b/packages/automas_script_hsr/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=68", "wheel"] +requires = ["setuptools>=77", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,11 +8,16 @@ version = "0.1.0" description = "HSR orchestration and adapter contracts for AUTO-MAS" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.12" -dependencies = ["pydantic>=2"] +authors = [{ name = "AUTO-MAS Team" }] +license = "AGPL-3.0-or-later" +license-files = ["LICENSE"] +dependencies = ["jinja2>=3.1", "pydantic>=2"] [project.urls] Homepage = "https://github.com/AUTO-MAS-Project/automas-hsr" Repository = "https://github.com/AUTO-MAS-Project/automas-hsr" +Documentation = "https://doc.auto-mas.top/" +Issues = "https://github.com/AUTO-MAS-Project/automas-hsr/issues" [project.entry-points."auto_mas.plugins"] automas_script_hsr = "automas_script_hsr.plugin:Plugin" diff --git a/packages/automas_script_hsr/src/automas_script_hsr/registry.py b/packages/automas_script_hsr/src/automas_script_hsr/registry.py index 2ec04f2..9109e51 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/registry.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/registry.py @@ -6,8 +6,6 @@ from pydantic import BaseModel -from app.core.script_types import ScriptRecordCapability - from .contracts import ( HSRAdapterGroup, HSRCapabilitySnapshot, @@ -236,15 +234,10 @@ def snapshot( def resolve_record_capability( self, config_data: dict[str, Any], - ) -> ScriptRecordCapability: + ) -> HSRCapabilitySnapshot: """供 ScriptTypeProvider 解析单条 HSR 脚本能力。""" - snapshot = self.snapshot(script_config=config_data) - return ScriptRecordCapability( - available=snapshot.available, - unavailable_reason=snapshot.unavailable_reason, - supported_modes=snapshot.supported_modes, - ) + return self.snapshot(script_config=config_data) @staticmethod def _normalize_engine(value: str) -> HSREngine: diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/autoproxy.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/autoproxy.py index 0f19e2e..fbc60d2 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/autoproxy.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/autoproxy.py @@ -802,7 +802,7 @@ def _build_login_plan( sra_available = bool(sra_path.strip()) and sra_exe_path.exists() if not sra_available: return HSRLoginPlan( - mode="m7a_fallback", + mode="m7a_current_account", sra_exe_path=sra_exe_path, ) diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/history.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/history.py new file mode 100644 index 0000000..70e4493 --- /dev/null +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/history.py @@ -0,0 +1,18 @@ +from __future__ import annotations + + +HSR_SUCCESS_LOG_STATUSES = frozenset( + { + "HSR 任务结束", + "HSR 用户任务完成", + "HSR 失败任务补跑完成", + "HSR 本轮无需执行,已跳过", + "HSR 人工排查通过", + } +) + + +def general_log_result(status: str) -> str: + """Map HSR success statuses to the host's generic history contract.""" + + return "Success!" if status in HSR_SUCCESS_LOG_STATUSES else status diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py index 0be8eb5..2b7bb20 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py @@ -36,6 +36,7 @@ from app.utils import get_logger from app.utils.constants import TASK_MODE_ZH, UTC4, UTC8 from .autoproxy import HSRAutoProxyTask +from .history import general_log_result from .manual_review import HSRManualReviewTask from .models import CompletionWriteback, HSRRuntimeState from .tasks import ( @@ -664,8 +665,10 @@ async def _persist_user_logs(self) -> None: / user_item.name / f"{dt.strftime('%H-%M-%S')}.log" ) - await Config.save_hsr_log( - log_path, log_item.content, log_item.status + await Config.save_general_log( + log_path, + log_item.content, + general_log_result(log_item.status), ) async def _restore_external_configs(self) -> str: diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/models.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/models.py index 9844332..120f861 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/models.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/models.py @@ -25,7 +25,7 @@ HSRPhase = Literal["daily", "weekly"] HSRScriptRunner = Literal["M7A", "SRA"] -HSRLoginMode = Literal["sra_switch", "sra_remembered", "m7a_fallback"] +HSRLoginMode = Literal["sra_switch", "sra_remembered", "m7a_current_account"] HSRModuleResultStatus = Literal["completed", "failed", "incomplete", "skipped"] diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/stage_contract.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/stage_contract.py index 06872a7..69aeb45 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/stage_contract.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/stage_contract.py @@ -6,7 +6,7 @@ def normalize_stage_categories(payload: dict[str, Any]) -> tuple[HSRStageCategory, ...]: - """Convert the legacy rich-editor stage payload to the public HSR contract.""" + """Convert the current stage-provider payload to the public HSR contract.""" categories: list[HSRStageCategory] = [] for raw_category in payload.get("categories", []): diff --git a/packages/automas_script_hsr/src/automas_script_hsr/schema.py b/packages/automas_script_hsr/src/automas_script_hsr/schema.py index 298cf27..875659f 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/schema.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/schema.py @@ -11,7 +11,7 @@ class HSRModel(BaseModel): - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="forbid") class HSRInfoConfig(HSRModel): diff --git a/pyproject.toml b/pyproject.toml index b793fb4..b1552b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,8 @@ [dependency-groups] dev = [ "build>=1.2", + "pip>=25", + "twine>=6.1", ] [tool.uv] diff --git a/scripts/build_all.py b/scripts/build_all.py index dd3e394..b5206d8 100644 --- a/scripts/build_all.py +++ b/scripts/build_all.py @@ -1,5 +1,6 @@ from __future__ import annotations +import shutil import subprocess import sys from pathlib import Path @@ -16,6 +17,8 @@ def main() -> None: ) for package in packages: output_dir = DIST_DIR / package.name + if output_dir.exists(): + shutil.rmtree(output_dir) output_dir.mkdir(parents=True, exist_ok=True) subprocess.run( [sys.executable, "-m", "build", str(package), "--outdir", str(output_dir)], diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 0000000..cd9631a --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import argparse +import os +import tomllib +from dataclasses import dataclass +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PACKAGE_CONFIG = { + "automas-script-hsr": ( + "packages/automas_script_hsr", + "pypi-script-hsr", + ), + "automas-hsr-adapter-sra": ( + "packages/automas_hsr_adapter_sra", + "pypi-adapter-sra", + ), + "automas-hsr-adapter-m7a": ( + "packages/automas_hsr_adapter_m7a", + "pypi-adapter-m7a", + ), +} + + +@dataclass(frozen=True, slots=True) +class ReleaseTarget: + package: str + package_dir: str + environment: str + version: str + + @property + def artifact_name(self) -> str: + return f"release-{self.package}-{self.version}" + + +def project_version(package_dir: str) -> str: + pyproject = ROOT / package_dir / "pyproject.toml" + data = tomllib.loads(pyproject.read_text(encoding="utf-8")) + return str(data["project"]["version"]) + + +def _tag_target(ref: str) -> tuple[str, str]: + prefix = "refs/tags/" + if not ref.startswith(prefix): + raise ValueError("tag release must run from refs/tags/-v") + tag = ref.removeprefix(prefix) + for package in PACKAGE_CONFIG: + package_prefix = f"{package}-v" + if tag.startswith(package_prefix): + version = tag.removeprefix(package_prefix) + if not version: + break + return package, version + raise ValueError(f"unsupported release tag: {tag}") + + +def resolve_release( + *, + event_name: str, + ref: str, + manual_package: str = "", + manual_version: str = "", +) -> ReleaseTarget: + if event_name == "workflow_dispatch": + if ref != "refs/heads/main": + raise ValueError("manual first release must run from the main branch") + package = manual_package.strip() + version = manual_version.strip() + if package not in PACKAGE_CONFIG: + raise ValueError(f"unsupported package: {package}") + if not version: + raise ValueError("manual release version is required") + elif event_name == "push": + package, version = _tag_target(ref) + else: + raise ValueError(f"unsupported release event: {event_name}") + + package_dir, environment = PACKAGE_CONFIG[package] + configured_version = project_version(package_dir) + if version != configured_version: + raise ValueError( + f"release version {version!r} does not match {package} " + f"project version {configured_version!r}" + ) + return ReleaseTarget( + package=package, + package_dir=package_dir, + environment=environment, + version=version, + ) + + +def _write_github_outputs(target: ReleaseTarget, output_path: Path) -> None: + values = { + "package": target.package, + "package_dir": target.package_dir, + "environment": target.environment, + "version": target.version, + "artifact_name": target.artifact_name, + } + with output_path.open("a", encoding="utf-8") as output: + for key, value in values.items(): + output.write(f"{key}={value}\n") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Resolve and validate a release target") + parser.add_argument("--event-name", required=True) + parser.add_argument("--ref", required=True) + parser.add_argument("--package", default="") + parser.add_argument("--version", default="") + args = parser.parse_args() + + target = resolve_release( + event_name=args.event_name, + ref=args.ref, + manual_package=args.package, + manual_version=args.version, + ) + github_output = os.environ.get("GITHUB_OUTPUT") + if github_output: + _write_github_outputs(target, Path(github_output)) + else: + print( + f"{target.package} {target.version} " + f"({target.package_dir}, {target.environment})" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/smoke_wheels.py b/scripts/smoke_wheels.py new file mode 100644 index 0000000..5c39607 --- /dev/null +++ b/scripts/smoke_wheels.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +import tempfile +import venv +import zipfile +from dataclasses import dataclass +from email.parser import BytesParser +from pathlib import Path + + +CORE_DISTRIBUTION = "automas-script-hsr" +ADAPTER_DISTRIBUTIONS = ( + "automas-hsr-adapter-sra", + "automas-hsr-adapter-m7a", +) +WORKSPACE_DISTRIBUTIONS = frozenset({CORE_DISTRIBUTION, *ADAPTER_DISTRIBUTIONS}) +ENTRY_POINTS = { + CORE_DISTRIBUTION: ( + "automas_script_hsr", + "automas_script_hsr.plugin:Plugin", + ), + "automas-hsr-adapter-sra": ( + "automas_hsr_adapter_sra", + "automas_hsr_adapter_sra.plugin:Plugin", + ), + "automas-hsr-adapter-m7a": ( + "automas_hsr_adapter_m7a", + "automas_hsr_adapter_m7a.plugin:Plugin", + ), +} +SMOKE_MODES = ("local-adapter-resolution", "metadata-only") +_REQUIREMENT_NAME_PATTERN = re.compile(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)") + + +@dataclass(frozen=True, slots=True) +class WheelMetadata: + name: str + version: str + requires_dist: tuple[str, ...] + + +def _canonical_name(value: str) -> str: + return re.sub(r"[-_.]+", "-", value).lower() + + +def _wheel_prefix(distribution: str) -> str: + return re.sub(r"[-_.]+", "_", distribution).lower() + "-" + + +def _find_wheel(dist_dir: Path, distribution: str) -> Path: + prefix = _wheel_prefix(distribution) + matches = sorted( + path + for path in dist_dir.rglob("*.whl") + if path.name.lower().startswith(prefix) + ) + if len(matches) != 1: + raise RuntimeError( + f"expected exactly one wheel for {distribution}, found {len(matches)}" + ) + return matches[0] + + +def _read_wheel_metadata(wheel: Path) -> WheelMetadata: + with zipfile.ZipFile(wheel) as archive: + metadata_paths = [ + name + for name in archive.namelist() + if name.endswith(".dist-info/METADATA") + ] + if len(metadata_paths) != 1: + raise RuntimeError( + f"expected exactly one METADATA file in {wheel.name}, " + f"found {len(metadata_paths)}" + ) + message = BytesParser().parsebytes(archive.read(metadata_paths[0])) + name = str(message.get("Name") or "").strip() + version = str(message.get("Version") or "").strip() + if not name or not version: + raise RuntimeError(f"wheel metadata is missing Name or Version: {wheel.name}") + return WheelMetadata( + name=name, + version=version, + requires_dist=tuple(message.get_all("Requires-Dist", [])), + ) + + +def _requirement_name(requirement: str) -> str: + match = _REQUIREMENT_NAME_PATTERN.match(requirement) + if match is None: + raise RuntimeError(f"cannot parse Requires-Dist entry: {requirement!r}") + return _canonical_name(match.group(1)) + + +def _external_requirements(metadata: list[WheelMetadata]) -> tuple[str, ...]: + requirements = { + requirement + for item in metadata + for requirement in item.requires_dist + if _requirement_name(requirement) not in WORKSPACE_DISTRIBUTIONS + } + return tuple(sorted(requirements, key=str.lower)) + + +def _venv_python(environment: Path) -> Path: + if os.name == "nt": + return environment / "Scripts" / "python.exe" + return environment / "bin" / "python" + + +def _expected_installation( + wheels: dict[str, Path], + distributions: tuple[str, ...], +) -> dict[str, dict[str, str]]: + expected: dict[str, dict[str, str]] = {} + for distribution in distributions: + entry_name, entry_value = ENTRY_POINTS[distribution] + metadata = _read_wheel_metadata(wheels[distribution]) + if _canonical_name(metadata.name) != distribution: + raise RuntimeError( + f"wheel name mismatch for {distribution}: {metadata.name!r}" + ) + expected[distribution] = { + "entry_name": entry_name, + "entry_value": entry_value, + "version": metadata.version, + } + return expected + + +def _inspect_installation( + python: Path, + expected: dict[str, dict[str, str]], +) -> None: + check_code = """ +import importlib.metadata +import importlib.util +import json +import sys + +expected = json.loads(sys.argv[1]) +for distribution, contract in expected.items(): + metadata = importlib.metadata.distribution(distribution) + if metadata.version != contract["version"]: + raise SystemExit( + f"unexpected version for {distribution}: {metadata.version!r}" + ) + entry_points = { + item.name: item.value + for item in metadata.entry_points + if item.group == "auto_mas.plugins" + } + if entry_points.get(contract["entry_name"]) != contract["entry_value"]: + raise SystemExit( + f"invalid entry point for {distribution}: {entry_points!r}" + ) + top_level_package = contract["entry_value"].partition(":")[0].partition(".")[0] + if importlib.util.find_spec(top_level_package) is None: + raise SystemExit(f"package module missing for {distribution}") +""" + subprocess.run( + [str(python), "-c", check_code, json.dumps(expected)], + check=True, + ) + + +def _create_environment(root: Path) -> Path: + environment = root / "venv" + venv.EnvBuilder(with_pip=True).create(environment) + return _venv_python(environment) + + +def smoke_metadata_only(dist_dir: Path, distributions: tuple[str, ...]) -> None: + wheels = { + distribution: _find_wheel(dist_dir, distribution) + for distribution in distributions + } + expected = _expected_installation(wheels, distributions) + with tempfile.TemporaryDirectory(prefix="automas-hsr-metadata-smoke-") as temp_dir: + python = _create_environment(Path(temp_dir)) + subprocess.run( + [ + str(python), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-deps", + *(str(wheels[distribution]) for distribution in distributions), + ], + check=True, + ) + _inspect_installation(python, expected) + + +def smoke_local_adapter_resolution( + dist_dir: Path, + adapters: tuple[str, ...] = ADAPTER_DISTRIBUTIONS, +) -> None: + invalid = [adapter for adapter in adapters if adapter not in ADAPTER_DISTRIBUTIONS] + if invalid: + raise ValueError(f"local dependency smoke only accepts adapters: {invalid}") + + required_distributions = (CORE_DISTRIBUTION, *adapters) + wheels = { + distribution: _find_wheel(dist_dir, distribution) + for distribution in required_distributions + } + wheel_metadata = [ + _read_wheel_metadata(wheels[distribution]) + for distribution in required_distributions + ] + external_requirements = _external_requirements(wheel_metadata) + + with tempfile.TemporaryDirectory(prefix="automas-hsr-local-deps-smoke-") as temp_dir: + temp_root = Path(temp_dir) + dependency_wheelhouse = temp_root / "external-wheelhouse" + dependency_wheelhouse.mkdir() + if external_requirements: + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "download", + "--disable-pip-version-check", + "--only-binary=:all:", + "--dest", + str(dependency_wheelhouse), + *external_requirements, + ], + check=True, + ) + + for adapter in adapters: + environment_root = temp_root / adapter + python = _create_environment(environment_root) + subprocess.run( + [ + str(python), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-index", + "--find-links", + str(dependency_wheelhouse), + "--find-links", + str(wheels[CORE_DISTRIBUTION].parent), + "--find-links", + str(wheels[adapter].parent), + str(wheels[adapter]), + ], + check=True, + ) + subprocess.run([str(python), "-m", "pip", "check"], check=True) + expected = _expected_installation( + wheels, + (CORE_DISTRIBUTION, adapter), + ) + _inspect_installation(python, expected) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Install and inspect built wheels") + parser.add_argument("dist_dir", type=Path) + parser.add_argument("--mode", required=True, choices=SMOKE_MODES) + parser.add_argument( + "--expected-package", + action="append", + choices=tuple(ENTRY_POINTS), + dest="expected_packages", + ) + args = parser.parse_args() + dist_dir = args.dist_dir.resolve() + + if args.mode == "metadata-only": + distributions = tuple(args.expected_packages or ENTRY_POINTS) + smoke_metadata_only(dist_dir, distributions) + print(f"metadata-only wheel smoke passed: {', '.join(distributions)}") + return + + adapters = tuple(args.expected_packages or ADAPTER_DISTRIBUTIONS) + smoke_local_adapter_resolution(dist_dir, adapters) + print( + "local adapter dependency resolution passed: " + f"{', '.join(adapters)} -> {CORE_DISTRIBUTION}" + ) + + +if __name__ == "__main__": + main() diff --git a/tests/test_history.py b/tests/test_history.py new file mode 100644 index 0000000..6deccbe --- /dev/null +++ b/tests/test_history.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + +from automas_script_hsr.runtime.history import ( + HSR_SUCCESS_LOG_STATUSES, + general_log_result, +) + + +class HistoryResultTests(unittest.TestCase): + def test_all_hsr_success_statuses_use_generic_success_result(self) -> None: + self.assertGreater(len(HSR_SUCCESS_LOG_STATUSES), 0) + for status in HSR_SUCCESS_LOG_STATUSES: + with self.subTest(status=status): + self.assertEqual(general_log_result(status), "Success!") + + def test_failure_status_is_preserved(self) -> None: + status = "HSR 运行异常: example" + self.assertEqual(general_log_result(status), status) + + def test_manager_uses_only_generic_history_writer(self) -> None: + manager = ( + Path(__file__).resolve().parents[1] + / "packages" + / "automas_script_hsr" + / "src" + / "automas_script_hsr" + / "runtime" + / "manager.py" + ).read_text(encoding="utf-8") + self.assertIn("Config.save_general_log", manager) + self.assertNotIn("Config.save_hsr_log", manager) diff --git a/tests/test_package_metadata.py b/tests/test_package_metadata.py index 2f86091..1c3762a 100644 --- a/tests/test_package_metadata.py +++ b/tests/test_package_metadata.py @@ -10,7 +10,7 @@ "automas_script_hsr": { "name": "automas-script-hsr", "entry_point": "automas_script_hsr.plugin:Plugin", - "dependencies": {"pydantic>=2"}, + "dependencies": {"jinja2>=3.1", "pydantic>=2"}, }, "automas_hsr_adapter_sra": { "name": "automas-hsr-adapter-sra", @@ -40,3 +40,55 @@ def test_package_metadata_and_entry_points(self) -> None: self.assertTrue( expected["dependencies"].issubset(set(project.get("dependencies", []))) ) + self.assertEqual(project["authors"], [{"name": "AUTO-MAS Team"}]) + self.assertEqual(project["license"], "AGPL-3.0-or-later") + self.assertEqual(project["license-files"], ["LICENSE"]) + package_license = pyproject.parent / "LICENSE" + self.assertIn( + "GNU AFFERO GENERAL PUBLIC LICENSE", + package_license.read_text(encoding="utf-8"), + ) + self.assertEqual( + project["urls"], + { + "Homepage": "https://github.com/AUTO-MAS-Project/automas-hsr", + "Repository": "https://github.com/AUTO-MAS-Project/automas-hsr", + "Documentation": "https://doc.auto-mas.top/", + "Issues": "https://github.com/AUTO-MAS-Project/automas-hsr/issues", + }, + ) + + def test_repository_contains_agpl_license_and_lockfile(self) -> None: + license_text = (ROOT / "LICENSE").read_text(encoding="utf-8") + self.assertIn("GNU AFFERO GENERAL PUBLIC LICENSE", license_text) + self.assertTrue((ROOT / "uv.lock").is_file()) + gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8") + self.assertNotIn("uv.lock", gitignore.splitlines()) + + def test_core_does_not_depend_on_host_script_record_type(self) -> None: + registry = ( + ROOT + / "packages" + / "automas_script_hsr" + / "src" + / "automas_script_hsr" + / "registry.py" + ).read_text(encoding="utf-8") + self.assertNotIn("app.core.script_types", registry) + self.assertNotIn("ScriptRecordCapability", registry) + self.assertIn( + ") -> HSRCapabilitySnapshot:", + registry, + ) + + def test_new_config_and_login_contracts_are_strictly_named(self) -> None: + core = ROOT / "packages" / "automas_script_hsr" / "src" / "automas_script_hsr" + schema = (core / "schema.py").read_text(encoding="utf-8") + models = (core / "runtime" / "models.py").read_text(encoding="utf-8") + autoproxy = (core / "runtime" / "autoproxy.py").read_text(encoding="utf-8") + self.assertIn('ConfigDict(extra="forbid")', schema) + self.assertNotIn('ConfigDict(extra="allow")', schema) + self.assertIn("m7a_current_account", models) + self.assertIn("m7a_current_account", autoproxy) + self.assertNotIn("m7a_fallback", models) + self.assertNotIn("m7a_fallback", autoproxy) diff --git a/tests/test_registry_contract.py b/tests/test_registry_contract.py new file mode 100644 index 0000000..5d57dfb --- /dev/null +++ b/tests/test_registry_contract.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import unittest + +from automas_script_hsr import HSRCapabilitySnapshot, HSRRegistryService + + +class RegistryContractTests(unittest.TestCase): + def test_record_resolver_returns_plugin_owned_duck_type(self) -> None: + registry = HSRRegistryService() + + capability = registry.resolve_record_capability( + {"SRA": {"Path": "C:/example/sra"}} + ) + + self.assertIsInstance(capability, HSRCapabilitySnapshot) + self.assertFalse(capability.available) + self.assertIsInstance(capability.supported_modes, tuple) + self.assertEqual(capability.configured_engines, ("SRA",)) diff --git a/tests/test_release.py b/tests/test_release.py new file mode 100644 index 0000000..8c45dd3 --- /dev/null +++ b/tests/test_release.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import unittest + +from scripts.release import PACKAGE_CONFIG, project_version, resolve_release + + +class ReleaseTargetTests(unittest.TestCase): + def _version(self, package: str) -> str: + package_dir, _ = PACKAGE_CONFIG[package] + return project_version(package_dir) + + def test_manual_first_release_must_run_from_main(self) -> None: + with self.assertRaisesRegex(ValueError, "main branch"): + resolve_release( + event_name="workflow_dispatch", + ref="refs/heads/feature/test", + manual_package="automas-script-hsr", + manual_version="0.1.0", + ) + + def test_manual_release_version_must_match_project(self) -> None: + with self.assertRaisesRegex(ValueError, "does not match"): + resolve_release( + event_name="workflow_dispatch", + ref="refs/heads/main", + manual_package="automas-script-hsr", + manual_version="999.0.0", + ) + + def test_supported_tags_resolve_to_their_package(self) -> None: + for package in PACKAGE_CONFIG: + with self.subTest(package=package): + version = self._version(package) + target = resolve_release( + event_name="push", + ref=f"refs/tags/{package}-v{version}", + ) + self.assertEqual(target.package, package) + self.assertEqual(target.version, version) + + def test_unknown_tag_is_rejected(self) -> None: + with self.assertRaisesRegex(ValueError, "unsupported release tag"): + resolve_release( + event_name="push", + ref="refs/tags/v0.1.0", + ) diff --git a/tests/test_stage_contract.py b/tests/test_stage_contract.py new file mode 100644 index 0000000..4b5abc3 --- /dev/null +++ b/tests/test_stage_contract.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import json +import tempfile +import unittest +from pathlib import Path + +from automas_hsr_adapter_m7a.stages import load_m7a_stage_options +from automas_hsr_adapter_sra.stages import load_sra_stage_options +from automas_script_hsr.runtime.stage_provider import ( + M7A_INSTANCE_TYPE_CALYX_GOLDEN, +) + + +class _ScriptConfig: + def __init__(self, **paths: str) -> None: + self._paths = paths + + def get(self, section: str, key: str) -> str: + if key != "Path": + return "" + return self._paths.get(section, "") + + +class CurrentStageProviderContractTests(unittest.TestCase): + def test_current_m7a_provider_payload_normalizes_to_public_dto(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + assets = root / "assets" / "config" + assets.mkdir(parents=True) + (assets / "instance_names.json").write_text( + json.dumps( + { + M7A_INSTANCE_TYPE_CALYX_GOLDEN: { + "Test Stage": "Test Detail", + } + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + (assets / "instance_drops.json").write_text("{}", encoding="utf-8") + + categories = load_m7a_stage_options(_ScriptConfig(M7A=str(root))) + + self.assertEqual(len(categories), 1) + option = categories[0].options[0] + self.assertEqual(option.label, "Test Stage") + self.assertEqual(option.detail, "Test Detail") + self.assertEqual( + option.native_payload["m7a"], + { + "instanceType": M7A_INSTANCE_TYPE_CALYX_GOLDEN, + "instanceName": "Test Stage", + }, + ) + + def test_current_sra_provider_payload_normalizes_to_public_dto(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + config_dir = root / "tasks" / "config" + config_dir.mkdir(parents=True) + (config_dir / "trailblaze_power.toml").write_text( + """ +[subtasks.calyx_golden] +name = "Test Category" +cost = 10 +max_count = 24 +levels = ["Test Stage"] +results = ["Test Detail"] +""".strip(), + encoding="utf-8", + ) + + categories = load_sra_stage_options(_ScriptConfig(SRA=str(root))) + + self.assertEqual(len(categories), 1) + option = categories[0].options[0] + self.assertEqual(option.label, "Test Stage") + self.assertEqual(option.detail, "Test Detail") + self.assertEqual(option.cost, 10) + self.assertEqual(option.max_count, 24) + self.assertEqual(option.native_payload["sra"], {"id": "calyx_golden", "level": 1}) diff --git a/tests/test_wheel_smoke.py b/tests/test_wheel_smoke.py new file mode 100644 index 0000000..7089e19 --- /dev/null +++ b/tests/test_wheel_smoke.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import tempfile +import unittest +import zipfile +from pathlib import Path + +from scripts.smoke_wheels import ( + ADAPTER_DISTRIBUTIONS, + CORE_DISTRIBUTION, + WheelMetadata, + _external_requirements, + _read_wheel_metadata, +) + + +class WheelSmokeContractTests(unittest.TestCase): + def test_workspace_dependencies_are_excluded_from_external_wheelhouse(self) -> None: + requirements = _external_requirements( + [ + WheelMetadata( + name="automas-script-hsr", + version="0.1.0", + requires_dist=("jinja2>=3.1", "pydantic>=2"), + ), + WheelMetadata( + name="automas-hsr-adapter-m7a", + version="0.1.0", + requires_dist=( + "automas-script-hsr<0.2.0,>=0.1.0", + "PyYAML>=6", + ), + ), + ] + ) + + self.assertEqual( + requirements, + ("jinja2>=3.1", "pydantic>=2", "PyYAML>=6"), + ) + + def test_wheel_metadata_reader_preserves_dependency_contract(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + wheel = Path(temp_dir) / "example-1.2.3-py3-none-any.whl" + with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr( + "example-1.2.3.dist-info/METADATA", + """Metadata-Version: 2.4 +Name: example +Version: 1.2.3 +Requires-Dist: automas-script-hsr>=0.1.0,<0.2.0 +""", + ) + + metadata = _read_wheel_metadata(wheel) + + self.assertEqual(metadata.name, "example") + self.assertEqual(metadata.version, "1.2.3") + self.assertEqual( + metadata.requires_dist, + ("automas-script-hsr>=0.1.0,<0.2.0",), + ) + + def test_ci_and_publish_use_distinct_smoke_modes(self) -> None: + root = Path(__file__).resolve().parents[1] + ci = (root / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ) + publish = (root / ".github" / "workflows" / "publish.yml").read_text( + encoding="utf-8" + ) + self.assertIn("--mode local-adapter-resolution", ci) + self.assertIn("--mode metadata-only", publish) + self.assertEqual( + ADAPTER_DISTRIBUTIONS, + ("automas-hsr-adapter-sra", "automas-hsr-adapter-m7a"), + ) + self.assertEqual(CORE_DISTRIBUTION, "automas-script-hsr") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ba39ff7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,785 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[manifest] +members = [ + "automas-hsr-adapter-m7a", + "automas-hsr-adapter-sra", + "automas-script-hsr", +] + +[manifest.dependency-groups] +dev = [ + { name = "build", specifier = ">=1.2" }, + { name = "pip", specifier = ">=25" }, + { name = "twine", specifier = ">=6.1" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "automas-hsr-adapter-m7a" +version = "0.1.0" +source = { editable = "packages/automas_hsr_adapter_m7a" } +dependencies = [ + { name = "automas-script-hsr" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "automas-script-hsr", editable = "packages/automas_script_hsr" }, + { name = "pyyaml", specifier = ">=6" }, +] + +[[package]] +name = "automas-hsr-adapter-sra" +version = "0.1.0" +source = { editable = "packages/automas_hsr_adapter_sra" } +dependencies = [ + { name = "automas-script-hsr" }, +] + +[package.metadata] +requires-dist = [{ name = "automas-script-hsr", editable = "packages/automas_script_hsr" }] + +[[package]] +name = "automas-script-hsr" +version = "0.1.0" +source = { editable = "packages/automas_script_hsr" } +dependencies = [ + { name = "jinja2" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "jinja2", specifier = ">=3.1" }, + { name = "pydantic", specifier = ">=2" }, +] + +[[package]] +name = "build" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/21/6ec54248b4d0d51f12f3ca4aa77a128077d747a5db86cb5a2fcd9aedecbd/build-1.5.1.tar.gz", hash = "sha256:94e17f1db803ab22f46049376c44c8437c52090f0dfdf1adc43df56542d644fb", size = 112439, upload-time = "2026-07-09T06:21:59.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/f7/2c3f99ff9282f1c1ec9f6298f2c03034658a0901eeacb3b3501cb488574c/build-1.5.1-py3-none-any.whl", hash = "sha256:f1a58fe2e5af5b0238a07b9e70207492c79ddebbdb1ad954fc86d62a56be3e0d", size = 31195, upload-time = "2026-07-09T06:21:58.551Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[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 = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, +] + +[[package]] +name = "docutils" +version = "0.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" }, +] + +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", size = 24684, upload-time = "2026-06-22T00:47:02.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", size = 1421679, upload-time = "2026-06-22T00:46:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", size = 792570, upload-time = "2026-06-22T00:46:22.179Z" }, + { url = "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", size = 842243, upload-time = "2026-06-22T00:46:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", size = 1001468, upload-time = "2026-06-22T00:46:25.481Z" }, + { url = "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", size = 1082933, upload-time = "2026-06-22T00:46:27.15Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", size = 1043120, upload-time = "2026-06-22T00:46:28.89Z" }, + { url = "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", size = 1023824, upload-time = "2026-06-22T00:46:30.453Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", size = 599253, upload-time = "2026-06-22T00:46:32.072Z" }, + { url = "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", size = 612553, upload-time = "2026-06-22T00:46:33.53Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", size = 595151, upload-time = "2026-06-22T00:46:34.878Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25", size = 1443564, upload-time = "2026-06-22T00:46:36.66Z" }, + { url = "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", size = 838002, upload-time = "2026-06-22T00:46:38.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", size = 823045, upload-time = "2026-06-22T00:46:39.495Z" }, + { url = "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", size = 1093171, upload-time = "2026-06-22T00:46:41.21Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", size = 1049217, upload-time = "2026-06-22T00:46:42.804Z" }, + { url = "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", size = 917372, upload-time = "2026-06-22T00:46:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", size = 806699, upload-time = "2026-06-22T00:46:45.99Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", size = 835165, upload-time = "2026-06-22T00:46:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", size = 858282, upload-time = "2026-06-22T00:46:49.276Z" }, + { url = "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", size = 1014328, upload-time = "2026-06-22T00:46:51.026Z" }, + { url = "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", size = 1098207, upload-time = "2026-06-22T00:46:52.674Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", size = 1056961, upload-time = "2026-06-22T00:46:54.335Z" }, + { url = "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", size = 1033829, upload-time = "2026-06-22T00:46:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", size = 609081, upload-time = "2026-06-22T00:46:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", size = 624461, upload-time = "2026-06-22T00:46:59.163Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" }, +] + +[[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 = "pip" +version = "26.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, +] + +[[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 = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[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 = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "readme-renderer" +version = "45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[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" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] From 89a5d7750a123d74879df9a28fa7765f90bd2945 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Wed, 15 Jul 2026 00:51:00 +0800 Subject: [PATCH 2/4] fix(hsr): preserve M7A multi-user execution --- .../automas_script_hsr/runtime/log_detect.py | 1 - .../src/automas_script_hsr/runtime/manager.py | 18 ------------------ tests/test_package_metadata.py | 5 +++++ 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/log_detect.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/log_detect.py index abd0a33..a980828 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/log_detect.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/log_detect.py @@ -61,7 +61,6 @@ "主循环超时", "强制退出", "未识别到战斗按钮", - "MemoryOfChaos 主循环失败", # ---- SRA 货币战争 final_failure(参考 HSR-外部脚本日志语义审计.md 2.5)---- "[页面定位] 检测超时", # CurrencyWars.py:159 "等待挑战结束超时", # CurrencyWars.py:708 diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py index 2b7bb20..8135337 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py @@ -371,13 +371,6 @@ def _is_executable_user(user_config) -> bool: and user_config.get("Info", "RemainedDay") != 0 ) - def _m7a_only_skip_reason(self, executable_user_index: int) -> str | None: - """Return the explicit skip reason after the first M7A-only account.""" - - if self.effective_engines == ("M7A",) and executable_user_index > 0: - return "M7A-only 仅支持当前登录账号" - return None - @staticmethod def _user_needs_sra(user_config, script_config: Any) -> bool: """判断用户是否需要 SRA StartGame 登录/切号。""" @@ -560,17 +553,6 @@ async def main_task(self): try: for user_index, user_item in enumerate(self.script_info.user_list): self.script_info.current_index = user_index - m7a_skip_reason = self._m7a_only_skip_reason(user_index) - if m7a_skip_reason is not None: - user_item.status = "跳过" - user_item.log_record[datetime.now()] = LogRecord( - status=f"跳过:{m7a_skip_reason}", - content=["M7A-only 只执行持久化顺序中第一个可用账号。\n"], - ) - self._append_log( - f"用户「{user_item.name}」跳过:{m7a_skip_reason}" - ) - continue proxy = None try: proxy = task_cls( diff --git a/tests/test_package_metadata.py b/tests/test_package_metadata.py index 1c3762a..0949223 100644 --- a/tests/test_package_metadata.py +++ b/tests/test_package_metadata.py @@ -86,9 +86,14 @@ def test_new_config_and_login_contracts_are_strictly_named(self) -> None: schema = (core / "schema.py").read_text(encoding="utf-8") models = (core / "runtime" / "models.py").read_text(encoding="utf-8") autoproxy = (core / "runtime" / "autoproxy.py").read_text(encoding="utf-8") + manager = (core / "runtime" / "manager.py").read_text(encoding="utf-8") + log_detect = (core / "runtime" / "log_detect.py").read_text(encoding="utf-8") self.assertIn('ConfigDict(extra="forbid")', schema) self.assertNotIn('ConfigDict(extra="allow")', schema) self.assertIn("m7a_current_account", models) self.assertIn("m7a_current_account", autoproxy) self.assertNotIn("m7a_fallback", models) self.assertNotIn("m7a_fallback", autoproxy) + self.assertNotIn("_m7a_only_skip_reason", manager) + self.assertNotIn("M7A-only 只执行", manager) + self.assertNotIn("MemoryOfChaos", log_detect) From 07bf33ca67958331f375a02097018347bfa51fc1 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Thu, 16 Jul 2026 11:49:13 +0800 Subject: [PATCH 3/4] refactor(hsr): enforce adapter runtime boundaries --- .../src/automas_hsr_adapter_m7a/control.py | 167 ++----------- .../src/automas_hsr_adapter_m7a/controller.py | 4 + .../src/automas_hsr_adapter_m7a/plugin.py | 53 +--- .../src/automas_hsr_adapter_m7a/runtime.py | 69 +++--- .../src/automas_hsr_adapter_sra/control.py | 189 ++++++-------- .../src/automas_hsr_adapter_sra/controller.py | 7 + .../src/automas_hsr_adapter_sra/plugin.py | 53 +--- .../src/automas_hsr_adapter_sra/runtime.py | 71 +++--- .../src/automas_script_hsr/__init__.py | 4 + .../src/automas_script_hsr/adapter.py | 30 --- .../src/automas_script_hsr/adapter_plugin.py | 69 ++++++ .../src/automas_script_hsr/contracts.py | 41 ++++ .../src/automas_script_hsr/plugin.py | 57 ++--- .../src/automas_script_hsr/registry.py | 2 +- .../automas_script_hsr/runtime/autoproxy.py | 66 ++--- .../src/automas_script_hsr/runtime/game.py | 93 +------ .../automas_script_hsr/runtime/log_detect.py | 19 +- .../src/automas_script_hsr/runtime/manager.py | 232 ++++-------------- .../runtime/manual_review.py | 90 +++++-- .../src/automas_script_hsr/runtime/models.py | 26 +- .../src/automas_script_hsr/runtime/tasks.py | 48 ++-- tests/test_adapter_boundary.py | 66 +++++ tests/test_runtime_contract.py | 201 +++++++++++++++ 23 files changed, 817 insertions(+), 840 deletions(-) delete mode 100644 packages/automas_script_hsr/src/automas_script_hsr/adapter.py create mode 100644 packages/automas_script_hsr/src/automas_script_hsr/adapter_plugin.py create mode 100644 tests/test_adapter_boundary.py create mode 100644 tests/test_runtime_contract.py diff --git a/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/control.py b/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/control.py index 2c3f805..270af00 100644 --- a/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/control.py +++ b/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/control.py @@ -21,20 +21,17 @@ import yaml -from app.models.task import UserItem from app.utils import get_logger +from automas_script_hsr.contracts import HSRNativeRunPlan from automas_script_hsr.runtime.models import ( - HSRPhase, HSRRetryableTaskError, - HSRRunItem, external_result_failure_summary, ) from automas_script_hsr.runtime.tasks import HSRTaskModule from . import config as m7a from automas_script_hsr.runtime.game import HSRAccountSwitcher -from automas_script_hsr.runtime.log_detect import detect_weekly_completion -from .runner import M7ARunner +from .runner import M7ACommandResult, M7ARunner from automas_script_hsr.runtime.stage_runtime import ( resolve_m7a_eow_stage, resolve_m7a_main_stage, @@ -44,32 +41,6 @@ logger = get_logger("HSR M7A 控制") -def _on_m7a_weekly_success( - result: object, - uid: str, - user_name: str, - module_name: str, - module_key: str, - queue_weekly_completion: Callable[[str, str, str], None], - record_module_result: Callable[..., None], -) -> None: - """M7A 差分宇宙 / 货币战争成功回调:先按日志判定再写完成态。""" - - completed, reason = detect_weekly_completion(result, "M7A", module_key) - if not completed: - record_module_result( - user_id=uid, - user_name=user_name, - module_key=module_key, - module_name=module_name, - script="M7A", - status="incomplete", - reason=reason, - ) - return - queue_weekly_completion(uid, user_name, module_name) - - class HSRM7AControl: """M7A 执行项创建与 config.yaml patch 控制。""" @@ -79,18 +50,10 @@ def __init__( script_config: Any, account_switcher: HSRAccountSwitcher, append_log: Callable[[str], None], - module_timeout_seconds: Callable[[str], int], - queue_eow_completion: Callable[[str, str, bool, object, str], None], - queue_weekly_completion: Callable[[str, str, str], None], - record_module_result: Callable[..., None], ) -> None: self.script_config = script_config self._account_switcher = account_switcher self._append_log = append_log - self._module_timeout_seconds = module_timeout_seconds - self._queue_eow_completion = queue_eow_completion - self._queue_weekly_completion = queue_weekly_completion - self._record_module_result = record_module_result async def run_m7a_command( self, @@ -99,15 +62,16 @@ async def run_m7a_command( module_name: str, command: str, timeout_seconds: int | None = None, - ): + ) -> M7ACommandResult: """执行一条 M7A 命令并同步调度台日志。""" await self._account_switcher.wait_before_external_script("M7A", user_name) self._append_log( f"用户「{user_name}」开始执行 M7A {module_name}({command})" ) - result = await m7a_runner.run_task(command, timeout=timeout_seconds or 600) - if getattr(result, "success", False): + timeout = 600 if timeout_seconds is None else timeout_seconds + result = await m7a_runner.run_task(command, timeout=timeout) + if result.success: self._append_log( f"用户「{user_name}」M7A {module_name}({command})执行完成" ) @@ -165,10 +129,10 @@ async def execute_m7a_daily( user_cfg: Any, user_name: str, module: HSRTaskModule, + timeout_seconds: int, m7a_path: str, m7a_runner: M7ARunner, daily_eow_enabled: bool, - timeout_seconds: int | None = None, ): """执行 M7A Daily 模块。""" @@ -182,7 +146,9 @@ async def execute_m7a_daily( eow_name=resolve_m7a_eow_stage(user_cfg), ) self.write_m7a_patch(m7a_config_path, daily_patch) - last_result: object | None = None + if not module.m7a_tasks: + raise RuntimeError(f"M7A {module.key} 未声明上游命令") + last_result: M7ACommandResult | None = None for command in module.m7a_tasks: result = await self.run_m7a_command( m7a_runner, @@ -200,40 +166,37 @@ async def execute_m7a_daily( result=result, ) + if last_result is None: + raise RuntimeError(f"M7A {module.key} 未执行任何上游命令") return last_result def create_patched_item( self, *, - user_item: UserItem, - user_cfg: Any, user_name: str, - uid: str, module: HSRTaskModule, - phase: HSRPhase, m7a_path: str, m7a_runner: M7ARunner, patch: dict, whitelist: frozenset[str], commands: list[str], - description: str, - on_success: Callable[[object], None] | None = None, - ) -> HSRRunItem: + ) -> HSRNativeRunPlan: """创建一个写入 M7A config.yaml patch 的队列项。""" - timeout_seconds = self._module_timeout_seconds(module.key) m7a_config_path = Path(m7a_path) / "config.yaml" async def run_m7a_patched(): if not m7a_config_path.exists(): raise RuntimeError(f"M7A config.yaml 不存在: {m7a_config_path}") + if not commands: + raise RuntimeError(f"M7A {module.key} 未声明上游命令") self.write_m7a_patch( m7a_config_path, patch, whitelist=whitelist, ) - last_result: object | None = None + last_result: M7ACommandResult | None = None for command in commands: result = await self.run_m7a_command( m7a_runner, @@ -245,40 +208,25 @@ async def run_m7a_patched(): last_result = result if not result.success: return result + if last_result is None: + raise RuntimeError(f"M7A {module.key} 未执行任何上游命令") return last_result - return HSRRunItem( - user_item=user_item, - user_cfg=user_cfg, - user_name=user_name, - user_id=uid, - phase=phase, - module_key=module.key, - module_name=module.name, - script="M7A", - description=description, - timeout_seconds=timeout_seconds, - run=run_m7a_patched, - on_success=on_success, - ) + return HSRNativeRunPlan(run=run_m7a_patched) def create_module_item( self, *, - user_item: UserItem, user_cfg: Any, user_name: str, - uid: str, module: HSRTaskModule, - phase: HSRPhase, + timeout_seconds: int, m7a_path: str, m7a_runner: M7ARunner, daily_eow_enabled: bool, - ) -> HSRRunItem | None: + ) -> HSRNativeRunPlan | None: """创建一个 M7A 模块队列项。""" - timeout_seconds = self._module_timeout_seconds(module.key) - if module.key == "Daily": daily_main_stage = resolve_m7a_main_stage(user_cfg) if daily_main_stage is None and not daily_eow_enabled: @@ -296,59 +244,25 @@ async def run_m7a_daily(): timeout_seconds=timeout_seconds, ) - return HSRRunItem( - user_item=user_item, - user_cfg=user_cfg, - user_name=user_name, - user_id=uid, - phase=phase, - module_key=module.key, - module_name=module.name, - script="M7A", - description=( - f"M7A routine:主关卡={'已配置' if daily_main_stage else '未配置'}," - f"历战余响本周尝试={'是' if daily_eow_enabled else '否'}" - ), - timeout_seconds=timeout_seconds, - run=run_m7a_daily, - on_success=( - lambda result, uid=uid, user_name=user_name, - daily_eow_enabled=daily_eow_enabled: - self._queue_eow_completion( - uid, - user_name, - daily_eow_enabled, - result, - "M7A", - ) - ), - extra={"daily_eow_enabled": daily_eow_enabled}, - ) + return HSRNativeRunPlan(run=run_m7a_daily) if module.key == "ReceiveRewards": return self.create_patched_item( - user_item=user_item, - user_cfg=user_cfg, user_name=user_name, - uid=uid, module=module, - phase=phase, + timeout_seconds=timeout_seconds, m7a_path=m7a_path, m7a_runner=m7a_runner, patch=m7a.build_receive_rewards_patch(user_cfg), whitelist=m7a.M7A_RECEIVE_REWARDS_PATCH_WHITELIST, commands=list(module.m7a_tasks), - description=f"M7A routine:{module.description}", ) if module.key == "DivergentUniverse": return self.create_patched_item( - user_item=user_item, - user_cfg=user_cfg, user_name=user_name, - uid=uid, module=module, - phase=phase, + timeout_seconds=timeout_seconds, m7a_path=m7a_path, m7a_runner=m7a_runner, patch=m7a.build_divergent_universe_patch( @@ -358,30 +272,13 @@ async def run_m7a_daily(): ), whitelist=m7a.M7A_COSMIC_STRIFE_PATCH_WHITELIST, commands=list(module.m7a_tasks), - description=f"M7A divergent:{module.description}", - on_success=( - lambda result, uid=uid, user_name=user_name, - module_name=module.name, module_key=module.key: - _on_m7a_weekly_success( - result, - uid, - user_name, - module_name, - module_key, - self._queue_weekly_completion, - self._record_module_result, - ) - ), ) if module.key == "CurrencyWars": return self.create_patched_item( - user_item=user_item, - user_cfg=user_cfg, user_name=user_name, - uid=uid, module=module, - phase=phase, + timeout_seconds=timeout_seconds, m7a_path=m7a_path, m7a_runner=m7a_runner, patch=m7a.build_currency_wars_patch( @@ -390,20 +287,6 @@ async def run_m7a_daily(): ), whitelist=m7a.M7A_COSMIC_STRIFE_PATCH_WHITELIST, commands=list(module.m7a_tasks), - description=f"M7A currencywars:{module.description}", - on_success=( - lambda result, uid=uid, user_name=user_name, - module_name=module.name, module_key=module.key: - _on_m7a_weekly_success( - result, - uid, - user_name, - module_name, - module_key, - self._queue_weekly_completion, - self._record_module_result, - ) - ), ) return None diff --git a/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/controller.py b/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/controller.py index c06cee9..e463bc2 100644 --- a/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/controller.py +++ b/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/controller.py @@ -20,6 +20,10 @@ def probe(self, script_config: Any) -> tuple[bool, str]: return False, f"三月七助手路径中未找到 March7th Assistant.exe:{executable}" return True, "" + def lock_paths(self, script_config: Any) -> tuple[str, ...]: + root = str(script_config.get("M7A", "Path") or "").strip() + return (root,) if root else () + async def open_session( self, *, diff --git a/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/plugin.py b/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/plugin.py index bb8a0f2..0b026d1 100644 --- a/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/plugin.py +++ b/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/plugin.py @@ -1,5 +1,7 @@ from __future__ import annotations +from automas_script_hsr.adapter_plugin import HSRAdapterPlugin + from .catalog import M7ATaskCatalog from .controller import M7AController @@ -21,49 +23,10 @@ } -class Plugin: +class Plugin(HSRAdapterPlugin): provides = ["hsr.task_catalog.m7a.v1", "hsr.controller.m7a.v1"] - needs = ["hsr.registry.v1"] - - def __init__(self, ctx) -> None: - self.ctx = ctx - self.catalog = M7ATaskCatalog() - self.controller = M7AController() - - async def on_start(self) -> None: - registry = self.ctx.get("hsr.registry.v1") - if registry is None: - raise RuntimeError("hsr.registry.v1 不可用") - registered = False - try: - registry.register_group( - owner=self.ctx.instance_id, - task_catalog=self.catalog, - controller=self.controller, - ) - registered = True - self.ctx.set("hsr.task_catalog.m7a.v1", self.catalog) - self.ctx.set("hsr.controller.m7a.v1", self.controller) - except Exception: - if registered: - registry.unregister_owner(self.ctx.instance_id) - self.ctx.set("hsr.task_catalog.m7a.v1", None) - self.ctx.set("hsr.controller.m7a.v1", None) - raise - self.ctx.logger.info("M7A HSR adapter ready") - - async def on_stop(self, reason: str) -> None: - registry = self.ctx.get("hsr.registry.v1") - if registry is not None: - cleanup_errors = await registry.close_owner_sessions(self.ctx.instance_id) - if cleanup_errors: - self.ctx.logger.error( - f"M7A HSR 活动会话清理失败: {';'.join(cleanup_errors)}" - ) - registry.unregister_owner(self.ctx.instance_id) - self.ctx.set("hsr.task_catalog.m7a.v1", None) - self.ctx.set("hsr.controller.m7a.v1", None) - self.ctx.logger.info(f"M7A HSR adapter stopped, reason={reason}") - - async def on_unload(self) -> None: - await self.on_stop("unload") + task_catalog_factory = M7ATaskCatalog + controller_factory = M7AController + task_catalog_service = "hsr.task_catalog.m7a.v1" + controller_service = "hsr.controller.m7a.v1" + display_name = "M7A HSR adapter" diff --git a/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/runtime.py b/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/runtime.py index 63ef32d..8099226 100644 --- a/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/runtime.py +++ b/packages/automas_hsr_adapter_m7a/src/automas_hsr_adapter_m7a/runtime.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import Any -from app.models.task import UserItem - from automas_script_hsr.contracts import HSRRunRequest, HSRRunResult from automas_script_hsr.runtime.tasks import get_module @@ -27,6 +25,7 @@ def __init__(self, *, script_id: str, script_config: Any, log, coordinator: Any) Path.cwd() / "data" / script_id / "Temp" / "M7A-session-config.yaml" ) self.config_existed = self.config_path.exists() + self._restored = False self._closed = False self.runner = M7ARunner( self.root, @@ -39,10 +38,6 @@ def __init__(self, *, script_id: str, script_config: Any, log, coordinator: Any) script_config=script_config, account_switcher=coordinator._account_switcher, append_log=log, - module_timeout_seconds=coordinator._module_timeout_seconds, - queue_eow_completion=coordinator._queue_eow_completion_if_confirmed, - queue_weekly_completion=coordinator._queue_weekly_completion, - record_module_result=coordinator._record_module_result, ) @classmethod @@ -54,11 +49,14 @@ async def create(cls, **kwargs) -> "M7AControllerSessionImpl": shutil.copy2(session.config_path, session.backup_path) session.coordinator.runtime.m7a_runner = session.runner return session - except Exception: + except Exception as setup_error: try: await session.close() - except Exception: - pass + except Exception as cleanup_error: + raise ExceptionGroup( + "M7A session setup and cleanup both failed", + [setup_error, cleanup_error], + ) from setup_error raise async def run(self, request: HSRRunRequest) -> HSRRunResult: @@ -66,29 +64,20 @@ async def run(self, request: HSRRunRequest) -> HSRRunResult: if module is None or not module.m7a_tasks: return HSRRunResult(status="skipped", summary="M7A 不提供该任务") item = self.control.create_module_item( - user_item=UserItem(request.user_id, request.user_name, "运行"), user_cfg=request.user_config, user_name=request.user_name, - uid=request.user_id, module=module, - phase=module.category, + timeout_seconds=request.timeout_seconds, m7a_path=str(self.root), m7a_runner=self.runner, daily_eow_enabled=bool(request.extra.get("daily_eow_enabled")), ) if item is None: return HSRRunResult(status="skipped", summary="M7A 任务无可执行内容") - result = await item.run() - if getattr(result, "success", False): - return HSRRunResult( - status="completed", - summary=str(getattr(result, "output", "") or "M7A 任务完成"), - completion_evidence={"returncode": getattr(result, "returncode", 0)}, - native_result=result, - ) - return HSRRunResult( - status="failed", - error=str(getattr(result, "error", "") or "M7A 任务失败"), + return HSRRunResult.from_native( + await item.run(), + default_summary="M7A 任务完成", + default_error="M7A 任务失败", ) async def cancel(self) -> None: @@ -104,22 +93,24 @@ async def close(self) -> None: except Exception as exc: # noqa: BLE001 errors.append(f"停止 M7A 子进程失败: {exc}") - try: - if self.config_existed: - if not self.backup_path.exists(): - raise RuntimeError(f"M7A 配置备份不存在: {self.backup_path}") - temp_path = self.config_path.with_name( - f"{self.config_path.name}.restore.tmp" - ) - shutil.copy2(self.backup_path, temp_path) - temp_path.replace(self.config_path) - elif self.config_path.exists(): - self.config_path.unlink() - if self.backup_path.exists(): - self.backup_path.unlink() - self._closed = True - except Exception as exc: # noqa: BLE001 - errors.append(f"恢复 M7A config.yaml 失败: {exc}") + if not self._restored: + try: + if self.config_existed: + if not self.backup_path.exists(): + raise RuntimeError(f"M7A 配置备份不存在: {self.backup_path}") + temp_path = self.config_path.with_name( + f"{self.config_path.name}.restore.tmp" + ) + shutil.copy2(self.backup_path, temp_path) + temp_path.replace(self.config_path) + elif self.config_path.exists(): + self.config_path.unlink() + if self.backup_path.exists(): + self.backup_path.unlink() + self._restored = True + except Exception as exc: # noqa: BLE001 + errors.append(f"恢复 M7A config.yaml 失败: {exc}") if errors: raise RuntimeError(";".join(errors)) + self._closed = True diff --git a/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/control.py b/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/control.py index d3ca981..11c1ce7 100644 --- a/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/control.py +++ b/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/control.py @@ -19,45 +19,19 @@ from pathlib import Path from typing import Any, Callable -from app.models.task import UserItem - -from automas_script_hsr.runtime.models import HSRPhase, HSRRunItem +from automas_script_hsr.contracts import HSRNativeRunPlan +from automas_script_hsr.runtime.models import HSRPhase from automas_script_hsr.runtime.tasks import HSRTaskModule from automas_script_hsr.runtime.game import HSRAccountSwitcher, resolve_sra_start_mode -from automas_script_hsr.runtime.log_detect import detect_weekly_completion from .runner import ( build_sra_module_config, - build_sra_tasklist_description, + build_sra_start_game_config, + run_sra_single_task, + SRAProcessRegistry, write_sra_temp_config, ) -def _on_sra_weekly_success( - result: object, - uid: str, - user_name: str, - module_name: str, - module_key: str, - queue_weekly_completion: Callable[[str, str, str], None], - record_module_result: Callable[..., None], -) -> None: - """SRA 差分宇宙 / 货币战争成功回调:先按日志判定再写完成态。""" - - completed, reason = detect_weekly_completion(result, "SRA", module_key) - if not completed: - record_module_result( - user_id=uid, - user_name=user_name, - module_key=module_key, - module_name=module_name, - script="SRA", - status="incomplete", - reason=reason, - ) - return - queue_weekly_completion(uid, user_name, module_name) - - class HSRSRAControl: """SRA 执行项创建与单任务控制。""" @@ -66,21 +40,13 @@ def __init__( *, script_config: Any, account_switcher: HSRAccountSwitcher, + process_registry: SRAProcessRegistry, append_log: Callable[[str], None], - phase_timeout_seconds: Callable[[HSRPhase], int], - module_timeout_seconds: Callable[[str], int], - queue_eow_completion: Callable[[str, str, bool, object, str], None], - queue_weekly_completion: Callable[[str, str, str], None], - record_module_result: Callable[..., None], ) -> None: self.script_config = script_config self._account_switcher = account_switcher + self._process_registry = process_registry self._append_log = append_log - self._phase_timeout_seconds = phase_timeout_seconds - self._module_timeout_seconds = module_timeout_seconds - self._queue_eow_completion = queue_eow_completion - self._queue_weekly_completion = queue_weekly_completion - self._record_module_result = record_module_result async def run_sra_task( self, @@ -91,43 +57,90 @@ async def run_sra_task( module_name: str, timeout_seconds: int | None = None, module_key: str = "", + track_script_switch: bool = True, ): """执行一条 SRA 单任务并同步调度台日志。""" - return await self._account_switcher.run_sra_task( + await self._account_switcher.wait_before_external_script( + "SRA", + user_name, + track_last_script=track_script_switch, + ) + self._append_log( + f"用户「{user_name}」开始执行 SRA {module_name}({task_class})" + ) + result = await run_sra_single_task( sra_exe_path, task_class, temp_path, + timeout=timeout_seconds or 600, + process_registry=self._process_registry, + log_callback=self._append_log, + output_line_callback=( + self._account_switcher.recover_game_window_if_screenshot_blocked + ), + module_key=module_key, + ) + state = "完成" if result.success else "失败" + self._append_log(f"用户「{user_name}」SRA {module_name} 执行{state}") + return result + + async def run_start_game( + self, + *, + user_config: Any, + user_name: str, + user_id: str, + script_id: str, + sra_exe_path: Path, + module_key: str, + temp_files: list[Path], + timeout_seconds: int, + ): + """Build and execute the SRA StartGame task inside the SRA adapter.""" + + await self._account_switcher.ensure_game_started_by_mas() + start_cfg = build_sra_start_game_config( + self.script_config, + user_config, + mode=resolve_sra_start_mode(user_config, user_name), + ) + temp_path = write_sra_temp_config( + start_cfg, + script_id, + user_id, + module_key, + ) + temp_files.append(temp_path) + result = await self.run_sra_task( + sra_exe_path, + "StartGameTask", + temp_path, user_name, - module_name, - timeout_seconds=timeout_seconds or 600, + "登录/切号", + timeout_seconds=timeout_seconds, module_key=module_key, + track_script_switch=False, ) + self._account_switcher.mark_game_session_clean(result.success) + return result def create_start_item( self, *, - user_item: UserItem, user_cfg: Any, user_name: str, uid: str, phase: HSRPhase, + timeout_seconds: int, sra_exe_path: Path, script_id: str, temp_files: list[Path], - ) -> HSRRunItem: + ) -> HSRNativeRunPlan: """创建 SRA 登录/切号队列项。""" - start_mode = resolve_sra_start_mode(user_cfg, user_name) - timeout_seconds = self._phase_timeout_seconds(phase) - - if start_mode == "switch": - description = "SRA StartGameTask:通过 MAS 密文登录/切号" - else: - description = "SRA StartGameTask:使用当前已记住账号启动/进入游戏(不切号)" - async def run_sra_start(): - return await self._account_switcher.run_start_game( + return await self.run_start_game( user_config=user_cfg, user_name=user_name, user_id=uid, @@ -138,37 +151,23 @@ async def run_sra_start(): timeout_seconds=timeout_seconds, ) - return HSRRunItem( - user_item=user_item, - user_cfg=user_cfg, - user_name=user_name, - user_id=uid, - phase=phase, - module_key="StartGame", - module_name="SRA 登录/切号", - script="SRA", - description=description, - timeout_seconds=timeout_seconds, - run=run_sra_start, - ) + return HSRNativeRunPlan(run=run_sra_start) def create_module_item( self, *, - user_item: UserItem, user_cfg: Any, user_name: str, uid: str, module: HSRTaskModule, - phase: HSRPhase, + timeout_seconds: int, sra_exe_path: Path, script_id: str, temp_files: list[Path], daily_eow_enabled: bool, - ) -> HSRRunItem | None: + ) -> HSRNativeRunPlan | None: """创建一个 SRA 模块队列项。""" - timeout_seconds = self._module_timeout_seconds(module.key) if module.key == "Daily": cfg = build_sra_module_config( module, @@ -180,10 +179,8 @@ def create_module_item( if not tasklist: self._append_log(f"用户「{user_name}」体力模块无可执行副本,跳过") return None - description = f"SRA TrailblazePowerTask:{build_sra_tasklist_description(tasklist)}" else: cfg = build_sra_module_config(module, self.script_config, user_cfg) - description = f"SRA {module.sra_task}:{module.description}" temp_path = write_sra_temp_config(cfg, script_id, uid, module.key) temp_files.append(temp_path) @@ -199,46 +196,4 @@ async def run_sra_module(): module_key=module.key, ) - on_success = None - if module.key == "Daily": - on_success = ( - lambda result, uid=uid, user_name=user_name, - daily_eow_enabled=daily_eow_enabled: - self._queue_eow_completion( - uid, - user_name, - daily_eow_enabled, - result, - "SRA", - ) - ) - elif module.key in ("DivergentUniverse", "CurrencyWars"): - on_success = ( - lambda result, uid=uid, user_name=user_name, - module_name=module.name, module_key=module.key: - _on_sra_weekly_success( - result, - uid, - user_name, - module_name, - module_key, - self._queue_weekly_completion, - self._record_module_result, - ) - ) - - return HSRRunItem( - user_item=user_item, - user_cfg=user_cfg, - user_name=user_name, - user_id=uid, - phase=phase, - module_key=module.key, - module_name=module.name, - script="SRA", - description=description, - timeout_seconds=timeout_seconds, - run=run_sra_module, - on_success=on_success, - extra={"daily_eow_enabled": daily_eow_enabled}, - ) + return HSRNativeRunPlan(run=run_sra_module) diff --git a/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/controller.py b/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/controller.py index dc37e93..2ae32ed 100644 --- a/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/controller.py +++ b/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/controller.py @@ -6,6 +6,7 @@ from automas_script_hsr import HSRControllerSession from .catalog import SRA_DESCRIPTOR +from .runner import get_sra_app_data_dir class SRAController: @@ -20,6 +21,12 @@ def probe(self, script_config: Any) -> tuple[bool, str]: return False, f"SRA 路径中未找到 SRA-cli.exe:{executable}" return True, "" + def lock_paths(self, script_config: Any) -> tuple[str, ...]: + root = str(script_config.get("SRA", "Path") or "").strip() + paths = [root] if root else [] + paths.append(str(get_sra_app_data_dir())) + return tuple(paths) + async def open_session( self, *, diff --git a/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/plugin.py b/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/plugin.py index 4ab39dc..da7839f 100644 --- a/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/plugin.py +++ b/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/plugin.py @@ -1,5 +1,7 @@ from __future__ import annotations +from automas_script_hsr.adapter_plugin import HSRAdapterPlugin + from .catalog import SRATaskCatalog from .controller import SRAController @@ -21,49 +23,10 @@ } -class Plugin: +class Plugin(HSRAdapterPlugin): provides = ["hsr.task_catalog.sra.v1", "hsr.controller.sra.v1"] - needs = ["hsr.registry.v1"] - - def __init__(self, ctx) -> None: - self.ctx = ctx - self.catalog = SRATaskCatalog() - self.controller = SRAController() - - async def on_start(self) -> None: - registry = self.ctx.get("hsr.registry.v1") - if registry is None: - raise RuntimeError("hsr.registry.v1 不可用") - registered = False - try: - registry.register_group( - owner=self.ctx.instance_id, - task_catalog=self.catalog, - controller=self.controller, - ) - registered = True - self.ctx.set("hsr.task_catalog.sra.v1", self.catalog) - self.ctx.set("hsr.controller.sra.v1", self.controller) - except Exception: - if registered: - registry.unregister_owner(self.ctx.instance_id) - self.ctx.set("hsr.task_catalog.sra.v1", None) - self.ctx.set("hsr.controller.sra.v1", None) - raise - self.ctx.logger.info("SRA HSR adapter ready") - - async def on_stop(self, reason: str) -> None: - registry = self.ctx.get("hsr.registry.v1") - if registry is not None: - cleanup_errors = await registry.close_owner_sessions(self.ctx.instance_id) - if cleanup_errors: - self.ctx.logger.error( - f"SRA HSR 活动会话清理失败: {';'.join(cleanup_errors)}" - ) - registry.unregister_owner(self.ctx.instance_id) - self.ctx.set("hsr.task_catalog.sra.v1", None) - self.ctx.set("hsr.controller.sra.v1", None) - self.ctx.logger.info(f"SRA HSR adapter stopped, reason={reason}") - - async def on_unload(self) -> None: - await self.on_stop("unload") + task_catalog_factory = SRATaskCatalog + controller_factory = SRAController + task_catalog_service = "hsr.task_catalog.sra.v1" + controller_service = "hsr.controller.sra.v1" + display_name = "SRA HSR adapter" diff --git a/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/runtime.py b/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/runtime.py index bb0f285..8932765 100644 --- a/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/runtime.py +++ b/packages/automas_hsr_adapter_sra/src/automas_hsr_adapter_sra/runtime.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import Any -from app.models.task import UserItem - from automas_script_hsr.contracts import HSRRunRequest, HSRRunResult from automas_script_hsr.runtime.tasks import get_module @@ -30,16 +28,13 @@ def __init__(self, *, script_id: str, script_config: Any, log, coordinator: Any) self.temp_files: list[Path] = [] self._backup_root = Path.cwd() / "data" / script_id / "Temp" / "SRA-session" self._backup_targets: list[tuple[Path, Path, bool]] = [] + self._restored = False self._closed = False self.control = HSRSRAControl( script_config=script_config, account_switcher=coordinator._account_switcher, + process_registry=self.process_registry, append_log=log, - phase_timeout_seconds=coordinator._phase_timeout_seconds, - module_timeout_seconds=coordinator._module_timeout_seconds, - queue_eow_completion=coordinator._queue_eow_completion_if_confirmed, - queue_weekly_completion=coordinator._queue_weekly_completion, - record_module_result=coordinator._record_module_result, ) @classmethod @@ -51,38 +46,44 @@ async def create(cls, **kwargs) -> "SRAControllerSessionImpl": coordinator = session.coordinator coordinator.runtime.sra_process_registry = session.process_registry return session - except Exception: + except Exception as setup_error: try: await session.close() - except Exception: - pass + except Exception as cleanup_error: + raise ExceptionGroup( + "SRA session setup and cleanup both failed", + [setup_error, cleanup_error], + ) from setup_error raise async def run(self, request: HSRRunRequest) -> HSRRunResult: root = Path(str(self.script_config.get("SRA", "Path") or "")) if request.task.key == "StartGame": item = self.control.create_start_item( - user_item=UserItem(request.user_id, request.user_name, "运行"), user_cfg=request.user_config, user_name=request.user_name, uid=request.user_id, phase=request.task.phase, + timeout_seconds=request.timeout_seconds, sra_exe_path=root / "SRA-cli.exe", script_id=request.script_id, temp_files=self.temp_files, ) - return self._normalize_result(await item.run(), "SRA 登录/切号完成") + return HSRRunResult.from_native( + await item.run(), + default_summary="SRA 登录/切号完成", + default_error="SRA 登录/切号失败", + ) module = get_module(request.task.key) if module is None or module.sra_task is None: return HSRRunResult(status="skipped", summary="SRA 不提供该任务") item = self.control.create_module_item( - user_item=UserItem(request.user_id, request.user_name, "运行"), user_cfg=request.user_config, user_name=request.user_name, uid=request.user_id, module=module, - phase=module.category, + timeout_seconds=request.timeout_seconds, sra_exe_path=root / "SRA-cli.exe", script_id=request.script_id, temp_files=self.temp_files, @@ -90,20 +91,10 @@ async def run(self, request: HSRRunRequest) -> HSRRunResult: ) if item is None: return HSRRunResult(status="skipped", summary="SRA 任务无可执行内容") - return self._normalize_result(await item.run(), "SRA 任务完成") - - @staticmethod - def _normalize_result(result: Any, default_summary: str) -> HSRRunResult: - if getattr(result, "success", False): - return HSRRunResult( - status="completed", - summary=str(getattr(result, "output", "") or default_summary), - completion_evidence={"returncode": getattr(result, "returncode", 0)}, - native_result=result, - ) - return HSRRunResult( - status="failed", - error=str(getattr(result, "error", "") or "SRA 任务失败"), + return HSRRunResult.from_native( + await item.run(), + default_summary="SRA 任务完成", + default_error="SRA 任务失败", ) async def cancel(self) -> None: @@ -123,17 +114,20 @@ async def close(self) -> None: cleanup_sra_temp_config(path) except Exception as exc: # noqa: BLE001 errors.append(f"清理 SRA 临时配置失败({path}): {exc}") - try: - self._restore_app_data() - self._closed = True - except Exception as exc: # noqa: BLE001 - errors.append(f"恢复 SRA AppData 失败: {exc}") + if not self._restored: + try: + self._restore_app_data() + self._restored = True + except Exception as exc: # noqa: BLE001 + errors.append(f"恢复 SRA AppData 失败: {exc}") if errors: raise RuntimeError(";".join(errors)) + self._closed = True def _backup_app_data(self) -> None: - shutil.rmtree(self._backup_root, ignore_errors=True) + if self._backup_root.exists(): + shutil.rmtree(self._backup_root) app_data = get_sra_app_data_dir() for name in ("settings.json", "cache.json", "configs"): source = app_data / name @@ -154,7 +148,7 @@ def _restore_app_data(self) -> None: try: if not existed: if source.is_dir(): - shutil.rmtree(source, ignore_errors=True) + shutil.rmtree(source) elif source.exists(): source.unlink() continue @@ -164,7 +158,7 @@ def _restore_app_data(self) -> None: source.parent.mkdir(parents=True, exist_ok=True) temp_path = source.with_name(f"{source.name}.restore.tmp") if temp_path.is_dir(): - shutil.rmtree(temp_path, ignore_errors=True) + shutil.rmtree(temp_path) elif temp_path.exists(): temp_path.unlink() if backup.is_dir(): @@ -172,7 +166,7 @@ def _restore_app_data(self) -> None: else: shutil.copy2(backup, temp_path) if source.is_dir(): - shutil.rmtree(source, ignore_errors=True) + shutil.rmtree(source) elif source.exists(): source.unlink() temp_path.replace(source) @@ -181,5 +175,6 @@ def _restore_app_data(self) -> None: if errors: raise RuntimeError(";".join(errors)) - shutil.rmtree(self._backup_root, ignore_errors=True) + if self._backup_root.exists(): + shutil.rmtree(self._backup_root) self._backup_targets.clear() diff --git a/packages/automas_script_hsr/src/automas_script_hsr/__init__.py b/packages/automas_script_hsr/src/automas_script_hsr/__init__.py index 1803494..5cdd961 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/__init__.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/__init__.py @@ -7,6 +7,8 @@ HSRController, HSRControllerSession, HSREngine, + HSRNativeRunPlan, + HSRNativeRunResult, HSRRunRequest, HSRRunResult, HSRStageCategory, @@ -23,6 +25,8 @@ "HSRController", "HSRControllerSession", "HSREngine", + "HSRNativeRunPlan", + "HSRNativeRunResult", "HSRRegistryService", "HSRRunRequest", "HSRRunResult", diff --git a/packages/automas_script_hsr/src/automas_script_hsr/adapter.py b/packages/automas_script_hsr/src/automas_script_hsr/adapter.py deleted file mode 100644 index 8f48173..0000000 --- a/packages/automas_script_hsr/src/automas_script_hsr/adapter.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -from app.plugins import ScriptAdapterHooks - - -class HSRAdapterHooks(ScriptAdapterHooks): - """HSR 统一编排钩子。""" - - async def check(self, runtime) -> str: - registry = runtime.get_service("hsr.registry.v1") - if registry is None: - return "请启用 HSR 核心插件" - - runtime.script_config = await runtime.build_script_model() - runtime.user_config = await runtime.storage.load_user_collection() - snapshot = registry.snapshot( - script_config=runtime.script_config, - ) - runtime.extra["hsr_capability_snapshot"] = snapshot - if not snapshot.available: - return snapshot.unavailable_reason or "请至少配置一个 HSR 引擎路径" - if runtime.mode not in snapshot.supported_modes: - return f"当前 HSR 引擎组合不支持任务模式 {runtime.mode}" - return "Pass" - - def run_auto_proxy(self, runtime): - raise RuntimeError("HSR 使用顺序多用户编排器,不通过 BaseAdapterManager 运行") - - def run_manual_review(self, runtime): - raise RuntimeError("HSR 使用顺序多用户编排器,不通过 BaseAdapterManager 运行") diff --git a/packages/automas_script_hsr/src/automas_script_hsr/adapter_plugin.py b/packages/automas_script_hsr/src/automas_script_hsr/adapter_plugin.py new file mode 100644 index 0000000..6f85dad --- /dev/null +++ b/packages/automas_script_hsr/src/automas_script_hsr/adapter_plugin.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from .contracts import HSRController, HSRTaskCatalogProvider + + +class HSRAdapterPlugin: + """Shared atomic lifecycle for one HSR task-catalog/controller pair.""" + + needs = ["hsr.registry.v1"] + task_catalog_factory: Callable[[], HSRTaskCatalogProvider] + controller_factory: Callable[[], HSRController] + task_catalog_service: str + controller_service: str + display_name: str + + def __init__(self, ctx: Any) -> None: + self.ctx = ctx + self.catalog = self.task_catalog_factory() + self.controller = self.controller_factory() + + async def on_start(self) -> None: + registry = self.ctx.get("hsr.registry.v1") + if registry is None: + raise RuntimeError("hsr.registry.v1 不可用") + + registered = False + try: + registry.register_group( + owner=self.ctx.instance_id, + task_catalog=self.catalog, + controller=self.controller, + ) + registered = True + self.ctx.set(self.task_catalog_service, self.catalog) + self.ctx.set(self.controller_service, self.controller) + except Exception: + if registered: + registry.unregister_owner(self.ctx.instance_id) + self._clear_services() + raise + self.ctx.logger.info(f"{self.display_name} ready") + + async def on_stop(self, reason: str) -> None: + await self._stop(reason=reason, final=False) + + async def on_unload(self) -> None: + await self._stop(reason="unload", final=True) + + async def _stop(self, *, reason: str, final: bool) -> None: + registry = self.ctx.get("hsr.registry.v1") + if registry is not None: + cleanup_errors = await registry.close_owner_sessions(self.ctx.instance_id) + if cleanup_errors: + self.ctx.logger.error( + f"{self.display_name} 活动会话清理失败: {';'.join(cleanup_errors)}" + ) + if not final: + return + registry.unregister_owner(self.ctx.instance_id) + + self._clear_services() + self.ctx.logger.info(f"{self.display_name} stopped, reason={reason}") + + def _clear_services(self) -> None: + self.ctx.set(self.task_catalog_service, None) + self.ctx.set(self.controller_service, None) diff --git a/packages/automas_script_hsr/src/automas_script_hsr/contracts.py b/packages/automas_script_hsr/src/automas_script_hsr/contracts.py index ab138c4..db79f7a 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/contracts.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/contracts.py @@ -85,6 +85,22 @@ class HSRRunRequest: extra: dict[str, Any] = field(default_factory=dict) +class HSRNativeRunResult(Protocol): + """Minimum result shape returned by an upstream engine runner.""" + + success: bool + output: str + error: str + returncode: int + + +@dataclass(frozen=True, slots=True) +class HSRNativeRunPlan: + """Adapter-owned native command ready to be executed once.""" + + run: Callable[[], Awaitable[HSRNativeRunResult]] + + @dataclass(frozen=True, slots=True) class HSRRunResult: """控制器返回的规范化结果。""" @@ -99,6 +115,29 @@ class HSRRunResult: def success(self) -> bool: return self.status == "completed" + @classmethod + def from_native( + cls, + result: HSRNativeRunResult, + *, + default_summary: str, + default_error: str, + ) -> "HSRRunResult": + """Normalize a typed upstream result without inventing missing evidence.""" + + if result.success: + return cls( + status="completed", + summary=str(result.output or default_summary), + completion_evidence={"returncode": result.returncode}, + native_result=result, + ) + return cls( + status="failed", + error=str(result.error or default_error), + native_result=result, + ) + def asdict(self) -> dict[str, Any]: return { "status": self.status, @@ -143,6 +182,8 @@ class HSRController(Protocol): def probe(self, script_config: Any) -> tuple[bool, str]: ... + def lock_paths(self, script_config: Any) -> tuple[str, ...]: ... + async def open_session( self, *, diff --git a/packages/automas_script_hsr/src/automas_script_hsr/plugin.py b/packages/automas_script_hsr/src/automas_script_hsr/plugin.py index 5180119..74e9e0d 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/plugin.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/plugin.py @@ -16,7 +16,6 @@ from app.plugins.realtime import schedule_plugin_snapshot from app.plugins.script_config_store import ScriptConfigStore -from .adapter import HSRAdapterHooks from .registry import HSRRegistryService from .schema import HSRConfig, HSRUserConfig @@ -52,7 +51,7 @@ def build_script_adapters(self) -> list[ScriptAdapterDefinition]: ScriptAdapterDefinition( type_key="HSR", display_name="HSR脚本", - hooks_factory=HSRAdapterHooks, + hooks_factory=None, script_model=HSRConfig, user_model=HSRUserConfig, script_class_name="HSRPluginConfig", @@ -63,6 +62,7 @@ def build_script_adapters(self) -> list[ScriptAdapterDefinition]: editor_kind="plugin:automas_script_hsr", is_builtin=False, record_capability_resolver=self.registry.resolve_record_capability, + manager_factory=self._build_manager, metadata={ "available": False, "unavailable_reason": "请启用 SRA 或 M7A HSR 适配器", @@ -74,21 +74,10 @@ def build_script_adapters(self) -> list[ScriptAdapterDefinition]: ] async def on_start(self) -> None: - definition = self.build_script_adapters()[0] - provider = definition.build_provider( - owner=self.ctx.instance_id, - plugin_context=self.ctx, - ) - from .runtime.manager import HSRManager - - provider.manager_factory = lambda script_item: HSRManager( - script_item, - provider=provider, - registry=self.registry, - ) - if provider.bind_related_config is not None: - provider.bind_related_config(Config) - script_type_registry.register(provider, owner=self.ctx.instance_id) + await super().on_start() + provider = script_type_registry.get("HSR") + if provider is None: + raise RuntimeError("HSR provider 注册失败") self.provider = provider self.registry.set_provider(provider) self.ctx.set("hsr.registry.v1", self.registry) @@ -106,26 +95,35 @@ async def on_start(self) -> None: self.ctx.logger.info("hsr.registry.v1 ready") async def on_stop(self, reason: str) -> None: - cleanup_errors = await self.registry.close_all_sessions() - if cleanup_errors: - self.ctx.logger.error( - f"HSR 活动会话清理失败: {';'.join(cleanup_errors)}" - ) - self.ctx.set("hsr.registry.v1", None) - removed = script_type_registry.unregister_by_owner(self.ctx.instance_id) - self.provider = None - self.ctx.logger.info(f"HSR 统一脚本入口已停止: {removed}, reason={reason}") + await self._stop(reason=reason, final=False) async def on_unload(self) -> None: + await self._stop(reason="unload", final=True) + + async def _stop(self, *, reason: str, final: bool) -> None: cleanup_errors = await self.registry.close_all_sessions() if cleanup_errors: self.ctx.logger.error( f"HSR 活动会话清理失败: {';'.join(cleanup_errors)}" ) + if not final: + return self.ctx.set("hsr.registry.v1", None) - removed = script_type_registry.unregister_by_owner(self.ctx.instance_id) + if final: + await super().on_unload() + else: + await super().on_stop(reason) self.provider = None - self.ctx.logger.info(f"HSR 统一脚本入口已卸载: {removed}") + self.ctx.logger.info(f"HSR 统一脚本入口已停止, reason={reason}") + + def _build_manager(self, script_item, provider): + from .runtime.manager import HSRManager + + return HSRManager( + script_item, + provider=provider, + registry=self.registry, + ) async def _capabilities(self, request: PluginHttpRequest) -> PluginHttpResponse: script_id = str(request.query.get("scriptId") or "").strip() @@ -133,8 +131,7 @@ async def _capabilities(self, request: PluginHttpRequest) -> PluginHttpResponse: return self._success(self.registry.snapshot().asdict()) try: - store, script_model = await self._load_script(script_id) - _ = store + _, script_model = await self._load_script(script_id) snapshot = self.registry.snapshot( script_config=script_model, ) diff --git a/packages/automas_script_hsr/src/automas_script_hsr/registry.py b/packages/automas_script_hsr/src/automas_script_hsr/registry.py index 9109e51..b22084d 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/registry.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/registry.py @@ -269,7 +269,7 @@ async def _close_sessions( await session.close() except Exception as exc: # noqa: BLE001 errors.append(f"{engine}: {type(exc).__name__}: {exc}") - finally: + else: self.release_session(engine, session) return tuple(errors) diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/autoproxy.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/autoproxy.py index fbc60d2..746c1ab 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/autoproxy.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/autoproxy.py @@ -36,6 +36,7 @@ from app.utils import ProcessManager, get_logger, is_process_running from app.utils.constants import UTC4, UTC8 from automas_script_hsr.contracts import ( + HSRNativeRunResult, HSRRunRequest, HSRRunResult, HSRTaskDescriptor, @@ -141,9 +142,7 @@ async def _open_adapter_sessions(self) -> None: if self.runtime.registry is None: raise RuntimeError("hsr.registry.v1 当前不可用") - effective = tuple( - getattr(self.script_config, "_hsr_effective_engines", ()) - ) + effective = self.runtime.effective_engines for engine in effective: group = self.runtime.registry.get_group(engine) session = await group.controller.open_session( @@ -152,8 +151,19 @@ async def _open_adapter_sessions(self) -> None: log=self._append_log, coordinator=self, ) - self.runtime.registry.track_session(engine, session) self.runtime.sessions[engine] = session + try: + self.runtime.registry.track_session(engine, session) + except Exception as setup_error: + try: + await session.close() + except Exception as cleanup_error: + raise ExceptionGroup( + f"HSR {engine} 会话注册与清理均失败", + [setup_error, cleanup_error], + ) from setup_error + self.runtime.sessions.pop(engine, None) + raise def _append_log(self, message: str, *, max_lines: int = 500) -> None: text = str(message).strip() @@ -463,7 +473,7 @@ def _queue_eow_completion_if_confirmed( user_id: str, user_name: str, eow_enabled: bool, - result: object, + result: HSRNativeRunResult, script: Literal["M7A", "SRA"], ) -> None: """外部脚本确认历战余响完成后,登记完成态。""" @@ -677,7 +687,11 @@ def _build_phase_items( continue if not user_cfg.get("TaskSwitch", module.key): continue - if not module_is_available(module, self.script_config): + if not module_is_available( + module, + self.script_config, + effective_engines=self.runtime.effective_engines, + ): reason = "当前脚本未启用提供该任务的引擎" self._append_log(f"用户「{user_name}」{module.name}跳过:{reason}") self._record_module_result( @@ -691,7 +705,11 @@ def _build_phase_items( ) continue - assigned = get_assigned_script(module, self.script_config) + assigned = get_assigned_script( + module, + self.script_config, + effective_engines=self.runtime.effective_engines, + ) module_daily_eow_enabled = daily_eow_enabled if module.key == "Daily": main_configured, module_daily_eow_enabled = ( @@ -717,9 +735,6 @@ def _build_phase_items( ) continue - async def unreachable_direct_run() -> object: - raise RuntimeError("HSR 普通任务必须通过控制器会话执行") - items.append( HSRRunItem( user_item=user_item, @@ -732,7 +747,6 @@ async def unreachable_direct_run() -> object: script=assigned, description=f"{assigned} {module.description}", timeout_seconds=self._module_timeout_seconds(module.key), - run=unreachable_direct_run, on_success=self._build_module_success_callback( module_key=module.key, module_name=module.name, @@ -768,7 +782,7 @@ def _build_module_success_callback( engine, ) if module_key in ("DivergentUniverse", "CurrencyWars"): - def on_weekly_success(result: object) -> None: + def on_weekly_success(result: HSRNativeRunResult) -> None: completed, reason = detect_weekly_completion( result, engine, @@ -828,9 +842,6 @@ def _create_start_game_item( timeout_seconds = self._timeout_seconds_for_phase(phase) - async def unreachable_direct_run() -> object: - raise RuntimeError("SRA 登录必须通过控制器会话执行") - return HSRRunItem( user_item=user_item, user_cfg=user_cfg, @@ -842,7 +853,6 @@ async def unreachable_direct_run() -> object: script="SRA", description="SRA StartGameTask:按当前用户凭证登录或使用已记住账号", timeout_seconds=timeout_seconds, - run=unreachable_direct_run, extra={"phase": phase}, ) @@ -1185,12 +1195,13 @@ async def _run_queue_items( ) continue - if bool(getattr(result, "success", True)): - if isinstance(result, HSRRunResult): - native_result = result.native_result - else: - native_result = result + if result.success: + native_result = result.native_result if item.on_success is not None: + if native_result is None: + raise HSRRetryableTaskError( + f"{item.script} 未返回业务完成判定所需的原生日志证据" + ) item.on_success(native_result) if item.module_key != "StartGame": semantic_result = next( @@ -1248,7 +1259,7 @@ async def _run_queue_items( return failures - async def _run_item_with_game_guard(self, item: HSRRunItem) -> object: + async def _run_item_with_game_guard(self, item: HSRRunItem) -> HSRRunResult: """执行单个外部模块;若游戏进程被关闭,尽快中止外部脚本。""" run_task = asyncio.create_task(self._execute_run_item(item)) @@ -1273,7 +1284,7 @@ async def _run_item_with_game_guard(self, item: HSRRunItem) -> object: await run_task raise - async def _execute_run_item(self, item: HSRRunItem) -> object: + async def _execute_run_item(self, item: HSRRunItem) -> HSRRunResult: """Execute every module, including SRA account login, via its session.""" session = self.runtime.sessions.get(item.script) @@ -1366,9 +1377,7 @@ async def main_task(self): uid = user_item.user_id user_cfg = self.cur_user_config user_name = user_cfg.get("Info", "Name") - effective_engines = set( - getattr(self.script_config, "_hsr_effective_engines", ()) - ) + effective_engines = set(self.runtime.effective_engines) sra_path = ( self.script_config.get("SRA", "Path") or "" if "SRA" in effective_engines @@ -1499,14 +1508,15 @@ async def final_task(self): await self._stop_external_processes() await self._push_user_statistics_notification() sessions = list(self.runtime.sessions.items()) - self.runtime.sessions.clear() for engine, session in reversed(sessions): try: await session.close() except Exception as e: # noqa: BLE001 logger.exception(f"关闭 HSR 适配器会话失败:{e}") self.crashed = True - finally: + else: + if self.runtime.sessions.get(engine) is session: + self.runtime.sessions.pop(engine, None) if self.runtime.registry is not None: self.runtime.registry.release_session(engine, session) for path in self.temp_files: diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/game.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/game.py index c14b40a..a322bf8 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/game.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/game.py @@ -153,9 +153,7 @@ async def stop_external_processes( path_checked = False if script_config is not None: - effective_engines = set( - getattr(script_config, "_hsr_effective_engines", ()) - ) + effective_engines = set(runtime.effective_engines) m7a_path = str(script_config.get("M7A", "Path") or "").strip() if m7a_path and "M7A" in effective_engines: m7a_exe_path = Path(m7a_path) / "March7th Assistant.exe" @@ -363,93 +361,10 @@ async def prepare_game_for_account_switch(self, user_name: str) -> None: async def close_game_if_needed(self) -> None: await close_game_if_needed(self.runtime, self.script_config, self._append_log) - async def run_sra_task( - self, - sra_exe_path: Path, - task_class: str, - temp_path: Path, - user_name: str, - module_name: str, - timeout_seconds: int | None = None, - module_key: str = "", - track_script_switch: bool = True, - ) -> Any: - """执行一条 SRA 单任务并同步调度台日志。""" - - from automas_hsr_adapter_sra.runner import run_sra_single_task - - await self.wait_before_external_script( - "SRA", - user_name, - track_last_script=track_script_switch, - ) - self._append_log( - f"用户「{user_name}」开始执行 SRA {module_name}({task_class})" - ) - result = await run_sra_single_task( - sra_exe_path, - task_class, - temp_path, - timeout=timeout_seconds or 600, - process_registry=self.runtime.sra_process_registry, - log_callback=self._append_log, - output_line_callback=self.recover_game_window_if_screenshot_blocked, - module_key=module_key, - ) - if result.success: - self._append_log( - f"用户「{user_name}」SRA {module_name} 执行完成" - ) - else: - self._append_log( - f"用户「{user_name}」SRA {module_name} 执行失败" - ) - return result - - async def run_start_game( - self, - *, - user_config: Any, - user_name: str, - user_id: str, - script_id: str, - sra_exe_path: Path, - module_key: str, - temp_files: list[Path], - timeout_seconds: int, - ) -> Any: - """只运行 SRA StartGameTask,用于自动代理前置切号与人工检查。""" - - from automas_hsr_adapter_sra.runner import ( - build_sra_start_game_config, - write_sra_temp_config, - ) + def mark_game_session_clean(self, clean: bool) -> None: + """Record whether the adapter completed a clean account-login transition.""" - await self.ensure_game_started_by_mas() - start_mode = resolve_sra_start_mode(user_config, user_name) - start_cfg = build_sra_start_game_config( - self.script_config, - user_config, - mode=start_mode, - ) - temp_path = write_sra_temp_config( - start_cfg, - script_id, - user_id, - module_key, - ) - temp_files.append(temp_path) - result = await self.run_sra_task( - sra_exe_path, - "StartGameTask", - temp_path, - user_name, - "登录/切号", - timeout_seconds=timeout_seconds, - track_script_switch=False, - ) - self.runtime.game_session_clean = bool(result.success) - return result + self.runtime.game_session_clean = bool(clean) async def _wait_after_game_process_detected(self, process_name: str) -> None: self._append_log( diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/log_detect.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/log_detect.py index a980828..1354733 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/log_detect.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/log_detect.py @@ -23,6 +23,8 @@ import re from typing import Callable +from automas_script_hsr.contracts import HSRNativeRunResult + M7A_COMPLETION_MARKERS: tuple[str, ...] = ("停止运行",) @@ -217,17 +219,18 @@ def has_screenshot_window_unavailable_output(text: str) -> bool: return all(marker in line for marker in HSR_SCREENSHOT_WINDOW_UNAVAILABLE_MARKERS) -def result_text(result: object) -> str: +def result_text(result: HSRNativeRunResult) -> str: """提取外部脚本 stdout/stderr 合并文本。""" - if result is None: - return "" - output = str(getattr(result, "output", "") or "") - error = str(getattr(result, "error", "") or "") + output = str(result.output or "") + error = str(result.error or "") return "\n".join(part for part in (output, error) if part) -def detect_echo_of_war_completion(result: object, script: str) -> tuple[bool, str]: +def detect_echo_of_war_completion( + result: HSRNativeRunResult, + script: str, +) -> tuple[bool, str]: """根据 M7A/SRA 输出判断本周历战余响是否已完成。""" text = result_text(result) @@ -295,7 +298,7 @@ def _parse_max_int(pattern: re.Pattern[str], text: str) -> int | None: def detect_weekly_completion( - result: object, + result: HSRNativeRunResult, script: str, module_key: str, ) -> tuple[bool, str]: @@ -347,7 +350,7 @@ def detect_weekly_completion( def detect_daily_completion( - result: object, + result: HSRNativeRunResult, script: str, ) -> tuple[bool, str]: """根据 M7A/SRA 输出判断日常领取模块是否已完成。 diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py index 8135337..63951ce 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/manager.py @@ -21,7 +21,6 @@ import asyncio -import shutil import uuid from datetime import datetime from pathlib import Path @@ -64,50 +63,16 @@ def _resolve_external_lock_paths( script_config: Any, effective_engines: tuple[str, ...], + registry: Any, ) -> list[str | Path]: """返回本轮 HSR 运行会修改的全部外部路径。""" - paths: list[str | Path] = [ - script_config.get(engine, "Path") - for engine in effective_engines - if script_config.get(engine, "Path") - ] - if "SRA" in effective_engines: - from automas_hsr_adapter_sra.runner import get_sra_app_data_dir - - paths.append(get_sra_app_data_dir()) + paths: list[str | Path] = [] + for engine in effective_engines: + paths.extend(registry.get_group(engine).controller.lock_paths(script_config)) return paths -def _remove_path(path: Path) -> None: - """删除文件或目录,供原子恢复前清理临时路径。""" - - if path.is_dir(): - shutil.rmtree(path, ignore_errors=True) - elif path.exists(): - path.unlink() - - -def _restore_path_from_backup(label: str, source: Path, backup: Path) -> None: - """用 tmp + rename 恢复一个外部配置路径。""" - - if not backup.exists(): - raise RuntimeError(f"备份路径不存在:{backup}") - - source.parent.mkdir(parents=True, exist_ok=True) - temp_source = source.with_name(f"{source.name}.tmp") - _remove_path(temp_source) - - if backup.is_dir(): - shutil.copytree(backup, temp_source) - else: - shutil.copy2(backup, temp_source) - - _remove_path(source) - temp_source.rename(source) - logger.info(f"{label} 已恢复:{source}") - - class HSRManager(TaskExecuteBase): """HSR 调度器,管理星穹铁道 M7A/SRA 双脚本任务。""" @@ -138,105 +103,8 @@ def __init__(self, script_info: ScriptItem, *, provider: Any, registry: Any): registry=registry, ) self.temp_path: Path = Path.cwd() / f"data/{self.script_info.script_id}/Temp" - self._external_config_targets: list[tuple[str, Path, Path, bool]] = [] self._external_path_locks: list[asyncio.Lock] = [] - def _backup_external_configs(self) -> None: - """运行前备份 M7A/SRA 的真实配置文件。""" - - if self.script_config is None: - return - - backup_root = self.temp_path / "ExternalConfig" - shutil.rmtree(backup_root, ignore_errors=True) - backup_root.mkdir(parents=True, exist_ok=True) - self._external_config_targets = [] - - def backup_path(label: str, source: Path, backup: Path) -> None: - existed = source.exists() - self._external_config_targets.append((label, source, backup, existed)) - if not existed: - logger.info(f"{label} 原配置不存在,记录为缺失:{source}") - return - - backup.parent.mkdir(parents=True, exist_ok=True) - if source.is_dir(): - shutil.copytree(source, backup, dirs_exist_ok=True) - elif source.is_file(): - shutil.copy2(source, backup) - else: - raise RuntimeError(f"{label} 既不是文件也不是目录:{source}") - logger.info(f"{label} 已备份:{source} -> {backup}") - - m7a_path = self.script_config.get("M7A", "Path") - if m7a_path and "M7A" in self.effective_engines: - backup_path( - "M7A config.yaml", - Path(str(m7a_path)) / "config.yaml", - backup_root / "M7A" / "config.yaml", - ) - - sra_path = self.script_config.get("SRA", "Path") - if sra_path and "SRA" in self.effective_engines: - from automas_hsr_adapter_sra.runner import get_sra_app_data_dir - - sra_app_data = get_sra_app_data_dir() - backup_path( - "SRA settings.json", - sra_app_data / "settings.json", - backup_root / "SRA" / "settings.json", - ) - backup_path( - "SRA cache.json", - sra_app_data / "cache.json", - backup_root / "SRA" / "cache.json", - ) - backup_path( - "SRA configs", - sra_app_data / "configs", - backup_root / "SRA" / "configs", - ) - - logger.info( - f"HSR 外部配置备份完成,共 {len(self._external_config_targets)} 项" - ) - - def _restore_external_config_targets(self) -> None: - """运行后恢复 M7A/SRA 配置,并清理备份目录。""" - - targets = list(self._external_config_targets) - if not targets: - return - - errors: list[str] = [] - for label, source, backup, existed in reversed(targets): - try: - if not existed: - if source.is_dir(): - shutil.rmtree(source, ignore_errors=True) - elif source.exists(): - source.unlink() - logger.info(f"{label} 原配置不存在,已清理任务期新增路径:{source}") - continue - - _restore_path_from_backup(label, source, backup) - except Exception as e: # noqa: BLE001 - errors.append(f"{label}: {e}") - logger.exception(f"恢复 HSR 外部配置失败:{label}: {e}") - - shutil.rmtree(self.temp_path / "ExternalConfig", ignore_errors=True) - try: - if self.temp_path.exists() and not any(self.temp_path.iterdir()): - self.temp_path.rmdir() - except OSError: - pass - - self._external_config_targets = [] - if errors: - raise RuntimeError(";".join(errors)) - - logger.info("HSR 外部配置已恢复") - def _append_log(self, message: str, *, max_lines: int = 500) -> None: """向调度台日志追加一行 HSR 运行信息。""" @@ -302,11 +170,7 @@ async def check(self) -> str: if self.task_info.mode not in snapshot.supported_modes: return f"当前 HSR 引擎组合不支持任务模式 {self.task_info.mode}" self.effective_engines = tuple(snapshot.effective_engines) - object.__setattr__( - self.script_config, - "_hsr_effective_engines", - self.effective_engines, - ) + self._runtime.effective_engines = self.effective_engines script_config = self.script_config for adapter in snapshot.adapters: @@ -317,7 +181,11 @@ async def check(self) -> str: return self._check_manual_review(script_config) for module in HSR_TASK_MODULES: - if not module_is_available(module, script_config): + if not module_is_available( + module, + script_config, + effective_engines=self.effective_engines, + ): continue raw_assigned = script_config._config_item_index["TaskMapping"][module.key].value if len(self.effective_engines) > 1 and not script_supports( @@ -341,7 +209,11 @@ async def check(self) -> str: for module in HSR_TASK_MODULES: if ( - module_is_available(module, script_config) + module_is_available( + module, + script_config, + effective_engines=self.effective_engines, + ) and user_config.get("TaskSwitch", module.key) ): enabled_module_keys.add(module.key) @@ -379,7 +251,12 @@ def _user_needs_sra(user_config, script_config: Any) -> bool: if not user_config.get("TaskSwitch", module.key): continue if ( - get_assigned_script(module, script_config) == "SRA" + get_assigned_script( + module, + script_config, + effective_engines=self.effective_engines, + ) + == "SRA" ): return True return False @@ -474,35 +351,18 @@ async def prepare(self): await self.store.lock() self.script_config = await self.store.load_script_model() self.user_config = await self.store.load_user_collection() - object.__setattr__( - self.script_config, - "_hsr_effective_engines", - self.effective_engines, - ) + self._runtime.effective_engines = self.effective_engines logger.success(f"{self.script_info.script_id} 已锁定,HSR 配置提取完成") external_roots = _resolve_external_lock_paths( self.script_config, self.effective_engines, + self.registry, ) self._external_path_locks = await acquire_external_path_locks(external_roots) self._append_log("HSR 外部脚本目录运行锁已获取") - self._backup_external_configs() - self._append_log("HSR 外部脚本配置已备份") - if "SRA" in self.effective_engines: - try: - from automas_hsr_adapter_sra.runner import ( - disable_sra_windows_notifications, - ) - - disable_sra_windows_notifications() - self._append_log("SRA 本体 Windows 通知已临时关闭") - except Exception as e: # noqa: BLE001 - logger.warning(f"SRA 本体 Windows 通知关闭失败:{e}") - self._append_log(f"SRA 本体 Windows 通知关闭失败,将继续执行:{e}") - self.script_info.user_list = [ UserItem( user_id=str(uid), @@ -584,6 +444,12 @@ async def main_task(self): continue if proxy.crashed: + cleanup_errors = await self._close_runtime_sessions() + if cleanup_errors: + raise RuntimeError( + "HSR 适配器会话清理失败,已停止后续用户:" + + ";".join(cleanup_errors) + ) error_message = proxy.error_message or "HSR 用户任务异常" user_errors.append(f"用户「{user_item.name}」执行异常:{error_message}") logger.error( @@ -653,21 +519,6 @@ async def _persist_user_logs(self) -> None: general_log_result(log_item.status), ) - async def _restore_external_configs(self) -> str: - """恢复 SRA / M7A 外部配置,返回错误文本或空串。""" - - if not self._external_config_targets: - return "" - - try: - self._restore_external_config_targets() - self._append_log("HSR 外部脚本配置已恢复") - return "" - except Exception as e: # noqa: BLE001 - logger.exception(f"HSR 外部脚本配置恢复失败:{e}") - self._append_log(f"HSR 外部脚本配置恢复失败:{e}") - return f"HSR 外部脚本配置恢复失败:{e}" - async def _sync_manual_review_user_data(self) -> None: """人工检查模式下,把本轮检查结果写回真实 UserData。""" @@ -751,6 +602,22 @@ async def _send_notification_error(self, message: str) -> None: except Exception as e: # noqa: BLE001 logger.warning(f"发送 HSR 通知错误提示失败:{e}") + async def _close_runtime_sessions(self) -> tuple[str, ...]: + """关闭本轮任务残留会话;失败项保留给下一次收口重试。""" + + errors: list[str] = [] + for engine, session in reversed(list(self._runtime.sessions.items())): + try: + await session.close() + except Exception as exc: # noqa: BLE001 + errors.append(f"{engine}: {type(exc).__name__}: {exc}") + continue + + if self._runtime.sessions.get(engine) is session: + self._runtime.sessions.pop(engine, None) + self.registry.release_session(engine, session) + return tuple(errors) + async def final_task(self): """解锁配置、恢复外部配置并落盘日志。""" @@ -771,9 +638,12 @@ async def final_task(self): self._append_log(msg) final_errors.append(msg) - restore_error = await self._restore_external_configs() - if restore_error: - final_errors.append(restore_error) + session_errors = await self._close_runtime_sessions() + if session_errors: + msg = "关闭 HSR 适配器会话失败:" + ";".join(session_errors) + logger.error(msg) + self._append_log(msg) + final_errors.append(msg) release_external_path_locks(self._external_path_locks) self._external_path_locks = [] diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/manual_review.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/manual_review.py index 9eb135e..434b9b0 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/manual_review.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/manual_review.py @@ -30,6 +30,7 @@ from app.models.task import LogRecord, ScriptItem, TaskExecuteBase, UserItem from app.utils import get_logger from app.utils.constants import UTC8 +from ..contracts import HSRRunRequest, HSRTaskDescriptor from .models import HSRRuntimeState from .game import HSRAccountSwitcher, check_user_credentials @@ -69,7 +70,7 @@ def __init__( ) self.message_queue: asyncio.Queue[dict] | None = None self._current_user_log: LogRecord | None = None - self.temp_files: list[Path] = [] + self._sra_session: Any | None = None self.check_result: str = "-" self.run_book: dict[str, bool] = {"SignIn": False, "PassCheck": False} self.crashed: bool = False @@ -133,6 +134,47 @@ async def prepare(self) -> None: self.message_queue = asyncio.Queue() await Broadcast.subscribe(self.message_queue) + if self.runtime.registry is None: + raise RuntimeError("hsr.registry.v1 当前不可用") + group = self.runtime.registry.get_group("SRA") + session = await group.controller.open_session( + script_id=self.script_info.script_id, + script_config=self.script_config, + log=self._append_log, + coordinator=self, + ) + self._sra_session = session + self.runtime.sessions["SRA"] = session + try: + self.runtime.registry.track_session("SRA", session) + except Exception as setup_error: + try: + await session.close() + except Exception as cleanup_error: + raise ExceptionGroup( + "HSR 人工排查 SRA 会话注册与清理均失败", + [setup_error, cleanup_error], + ) from setup_error + self._sra_session = None + self.runtime.sessions.pop("SRA", None) + raise + + async def _close_sra_session(self) -> str: + session = self._sra_session + if session is None: + return "" + try: + await session.close() + except Exception as exc: # noqa: BLE001 + logger.exception(f"关闭 HSR 人工排查 SRA 会话失败: {exc}") + return str(exc) + self._sra_session = None + if self.runtime.sessions.get("SRA") is session: + self.runtime.sessions.pop("SRA", None) + if self.runtime.registry is not None: + self.runtime.registry.release_session("SRA", session) + return "" + async def _unsubscribe_broadcast(self) -> None: """释放人工排查消息订阅,避免旧队列持续接收广播。""" @@ -204,28 +246,37 @@ async def main_task(self) -> None: await self.prepare() self._start_user_log() - sra_exe_path = Path(self.script_config.get("SRA", "Path")) / "SRA-cli.exe" + if self._sra_session is None: + raise RuntimeError("HSR 人工排查 SRA 会话未初始化") while True: self._append_log(f"正在启动游戏并切换到用户「{self.cur_user_item.name}」") await self._account_switcher.prepare_game_for_account_switch( self.cur_user_item.name ) - result = await self._account_switcher.run_start_game( - user_config=self.cur_user_config, - user_name=self.cur_user_item.name, - user_id=self.cur_user_item.user_id, - script_id=self.script_info.script_id, - sra_exe_path=sra_exe_path, - module_key="ManualReview_StartGame", - temp_files=self.temp_files, - timeout_seconds=self._start_game_timeout_seconds(), + result = await self._sra_session.run( + HSRRunRequest( + script_id=self.script_info.script_id, + user_id=self.cur_user_item.user_id, + user_name=self.cur_user_item.name, + task=HSRTaskDescriptor( + key="StartGame", + name="SRA 登录/切号", + phase="daily", + description="人工排查前登录/切号", + native_tasks=("StartGameTask",), + ), + timeout_seconds=self._start_game_timeout_seconds(), + script_config=self.script_config, + user_config=self.cur_user_config, + log=self._append_log, + ) ) if result.success: self.run_book["SignIn"] = True self._append_log(f"用户「{self.cur_user_item.name}」登录/切号完成") break - reason = result.error or result.output or f"进程退出码:{result.returncode}" + reason = result.error or result.summary or "SRA 未返回失败摘要" self._append_log( f"用户「{self.cur_user_item.name}」登录/切号失败:{reason}" ) @@ -243,24 +294,24 @@ async def main_task(self) -> None: async def final_task(self) -> str | None: """写回人工检查结果并清理临时 SRA 配置。""" + result: str | None = None try: - for temp_path in self.temp_files: - temp_path.unlink(missing_ok=True) - if self.check_result != "Pass": await self.cur_user_config.set("Data", "IfPassCheck", False) self._finish_user_log(self.check_result, "异常") - return self.check_result - - if self.run_book["SignIn"] and self.run_book["PassCheck"]: + result = self.check_result + elif self.run_book["SignIn"] and self.run_book["PassCheck"]: await self.cur_user_config.set("Data", "IfPassCheck", True) self._finish_user_log("HSR 人工排查通过", "完成") else: await self.cur_user_config.set("Data", "IfPassCheck", False) self._finish_user_log("HSR 人工排查未通过", "异常") - return None finally: + cleanup_error = await self._close_sra_session() await self._unsubscribe_broadcast() + if cleanup_error: + raise RuntimeError(f"HSR 人工排查 SRA 会话清理失败: {cleanup_error}") + return result async def on_crash(self, e: Exception) -> None: self.crashed = True @@ -286,4 +337,5 @@ async def on_crash(self, e: Exception) -> None: except Exception as notify_error: # noqa: BLE001 logger.warning(f"发送 HSR 人工排查异常提示失败:{notify_error}") finally: + await self._close_sra_session() await self._unsubscribe_broadcast() diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/models.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/models.py index 120f861..2572086 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/models.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/models.py @@ -18,10 +18,11 @@ from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Awaitable, Callable, Literal +from typing import Any, Callable, Literal from app.models.task import UserItem from app.utils import ProcessManager +from automas_script_hsr.contracts import HSRNativeRunResult, HSRRunResult HSRPhase = Literal["daily", "weekly"] HSRScriptRunner = Literal["M7A", "SRA"] @@ -86,8 +87,7 @@ class HSRRunItem: script: HSRScriptRunner description: str timeout_seconds: int - run: Callable[[], Awaitable[object]] - on_success: Callable[[object], None] | None = None + on_success: Callable[[HSRNativeRunResult], None] | None = None extra: dict[str, Any] = field(default_factory=dict) last_error: str = "" attempts: int = 0 @@ -100,7 +100,7 @@ def __init__( self, message: str, *, - result: object | None = None, + result: HSRRunResult | HSRNativeRunResult | None = None, ) -> None: super().__init__(message) self.result = result @@ -113,6 +113,7 @@ class HSRRuntimeState: log_lines: list[str] completion_writebacks: list[CompletionWriteback] module_results: list[HSRModuleResult] = field(default_factory=list) + effective_engines: tuple[HSRScriptRunner, ...] = () registry: Any = None sessions: dict[str, Any] = field(default_factory=dict) m7a_runner: Any = None @@ -139,14 +140,19 @@ def record_module_result(self, result: HSRModuleResult) -> None: self.module_results.append(result) -def external_result_failure_summary(result: object) -> str: +def external_result_failure_summary( + result: HSRRunResult | HSRNativeRunResult, +) -> str: """提取外部脚本结果中的失败摘要,避免整段日志刷屏。""" - if result is None: - return "未知错误" - error = str(getattr(result, "error", "") or "").strip() - output = str(getattr(result, "output", "") or "").strip() - returncode = getattr(result, "returncode", None) + if isinstance(result, HSRRunResult): + error = result.error.strip() + output = result.summary.strip() + returncode = result.completion_evidence.get("returncode") + else: + error = str(result.error or "").strip() + output = str(result.output or "").strip() + returncode = result.returncode text = error or output if not text and returncode not in (None, 0): text = f"进程退出码:{returncode}" diff --git a/packages/automas_script_hsr/src/automas_script_hsr/runtime/tasks.py b/packages/automas_script_hsr/src/automas_script_hsr/runtime/tasks.py index ff1a0c5..879721c 100644 --- a/packages/automas_script_hsr/src/automas_script_hsr/runtime/tasks.py +++ b/packages/automas_script_hsr/src/automas_script_hsr/runtime/tasks.py @@ -116,17 +116,31 @@ def script_supports(module_key: str, script: ScriptType) -> bool: return script in module.supported_scripts -def get_assigned_script(module: HSRTaskModule, script_config) -> ScriptType: +def _resolve_effective_engines( + script_config, + effective_engines: tuple[ScriptType, ...] | None, +) -> tuple[ScriptType, ...]: + engines = ( + resolve_configured_engines(script_config) + if effective_engines is None + else effective_engines + ) + return tuple( + engine + for item in engines + if (engine := str(item).upper()) in ("SRA", "M7A") + ) # type: ignore[return-value] + + +def get_assigned_script( + module: HSRTaskModule, + script_config, + *, + effective_engines: tuple[ScriptType, ...] | None = None, +) -> ScriptType: """Resolve a module to an effective engine for the current script.""" - effective = tuple( - str(item).upper() - for item in ( - getattr(script_config, "_hsr_effective_engines", None) - or resolve_configured_engines(script_config) - or () - ) - ) + effective = _resolve_effective_engines(script_config, effective_engines) supported = tuple( engine for engine in effective if engine in module.supported_scripts ) @@ -141,15 +155,13 @@ def get_assigned_script(module: HSRTaskModule, script_config) -> ScriptType: return normalized -def module_is_available(module: HSRTaskModule, script_config) -> bool: +def module_is_available( + module: HSRTaskModule, + script_config, + *, + effective_engines: tuple[ScriptType, ...] | None = None, +) -> bool: """Return whether at least one effective engine provides the module.""" - effective = { - str(item).upper() - for item in ( - getattr(script_config, "_hsr_effective_engines", None) - or resolve_configured_engines(script_config) - or () - ) - } + effective = set(_resolve_effective_engines(script_config, effective_engines)) return bool(effective.intersection(module.supported_scripts)) diff --git a/tests/test_adapter_boundary.py b/tests/test_adapter_boundary.py new file mode 100644 index 0000000..b0779cc --- /dev/null +++ b/tests/test_adapter_boundary.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +CORE = ROOT / "packages" / "automas_script_hsr" / "src" / "automas_script_hsr" +ADAPTERS = ( + ROOT / "packages" / "automas_hsr_adapter_sra" / "src" / "automas_hsr_adapter_sra", + ROOT / "packages" / "automas_hsr_adapter_m7a" / "src" / "automas_hsr_adapter_m7a", +) + + +class AdapterBoundaryTests(unittest.TestCase): + def test_core_uses_the_host_manager_factory_contract(self) -> None: + plugin = (CORE / "plugin.py").read_text(encoding="utf-8") + + self.assertIn("hooks_factory=None", plugin) + self.assertIn("manager_factory=self._build_manager", plugin) + self.assertIn("await super().on_start()", plugin) + self.assertNotIn("provider.manager_factory =", plugin) + + def test_engine_adapters_share_the_atomic_lifecycle(self) -> None: + self.assertTrue((CORE / "adapter_plugin.py").is_file()) + self.assertFalse((CORE / "adapter.py").exists()) + + for adapter in ADAPTERS: + with self.subTest(adapter=adapter.name): + plugin = (adapter / "plugin.py").read_text(encoding="utf-8") + self.assertIn( + "from automas_script_hsr.adapter_plugin import HSRAdapterPlugin", + plugin, + ) + self.assertIn("class Plugin(HSRAdapterPlugin):", plugin) + + def test_native_execution_contract_is_owned_by_the_core_package(self) -> None: + contracts = (CORE / "contracts.py").read_text(encoding="utf-8") + + self.assertIn("class HSRNativeRunPlan", contracts) + self.assertIn("class HSRNativeRunResult", contracts) + + def test_failed_session_cleanup_keeps_references_for_manager_retry(self) -> None: + autoproxy = (CORE / "runtime" / "autoproxy.py").read_text( + encoding="utf-8" + ) + manager = (CORE / "runtime" / "manager.py").read_text(encoding="utf-8") + manual_review = (CORE / "runtime" / "manual_review.py").read_text( + encoding="utf-8" + ) + + self.assertNotIn("self.runtime.sessions.clear()", autoproxy) + self.assertIn("cleanup_errors = await self._close_runtime_sessions()", manager) + self.assertIn("已停止后续用户", manager) + + close_start = manual_review.index("async def _close_sra_session") + close_end = manual_review.index("async def _unsubscribe_broadcast", close_start) + close_block = manual_review[close_start:close_end] + self.assertLess( + close_block.index("await session.close()"), + close_block.index("self._sra_session = None"), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime_contract.py b/tests/test_runtime_contract.py new file mode 100644 index 0000000..8fd7b38 --- /dev/null +++ b/tests/test_runtime_contract.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +import unittest +from dataclasses import dataclass +from typing import Any + +from automas_script_hsr.adapter_plugin import HSRAdapterPlugin +from automas_script_hsr.contracts import ( + HSRAdapterDescriptor, + HSRRunResult, +) +from automas_script_hsr.registry import HSRRegistryService + + +DESCRIPTOR = HSRAdapterDescriptor( + engine="SRA", + display_name="SRA", + version="test", + tasks=(), +) + + +class _Catalog: + descriptor = DESCRIPTOR + + def list_tasks(self): + return () + + def list_stage_options(self, **kwargs): + _ = kwargs + return () + + +class _Controller: + descriptor = DESCRIPTOR + + def probe(self, script_config): + _ = script_config + return True, "" + + def lock_paths(self, script_config): + _ = script_config + return () + + async def open_session(self, **kwargs): + _ = kwargs + raise NotImplementedError + + +class _RetryableSession: + def __init__(self) -> None: + self.close_calls = 0 + + async def close(self) -> None: + self.close_calls += 1 + if self.close_calls == 1: + raise RuntimeError("busy") + + +class RegistrySessionTests(unittest.IsolatedAsyncioTestCase): + async def test_failed_close_remains_tracked_for_a_later_retry(self) -> None: + registry = HSRRegistryService() + registry.register_group( + owner="adapter", + task_catalog=_Catalog(), + controller=_Controller(), + ) + session = _RetryableSession() + registry.track_session("SRA", session) + + first_errors = await registry.close_owner_sessions("adapter") + second_errors = await registry.close_owner_sessions("adapter") + third_errors = await registry.close_owner_sessions("adapter") + + self.assertEqual(first_errors, ("SRA: RuntimeError: busy",)) + self.assertEqual(second_errors, ()) + self.assertEqual(third_errors, ()) + self.assertEqual(session.close_calls, 2) + + +class _Logger: + def __init__(self) -> None: + self.errors: list[str] = [] + + def info(self, message: str) -> None: + _ = message + + def error(self, message: str) -> None: + self.errors.append(message) + + +class _LifecycleRegistry: + def __init__(self) -> None: + self.owner: str | None = None + self.close_results: list[tuple[str, ...]] = [] + self.unregister_calls = 0 + + def register_group(self, *, owner, task_catalog, controller) -> None: + _ = task_catalog, controller + self.owner = owner + + async def close_owner_sessions(self, owner: str) -> tuple[str, ...]: + self.assert_owner(owner) + return self.close_results.pop(0) if self.close_results else () + + def unregister_owner(self, owner: str) -> None: + self.assert_owner(owner) + self.owner = None + self.unregister_calls += 1 + + def assert_owner(self, owner: str) -> None: + if self.owner != owner: + raise AssertionError(f"unexpected owner: {owner!r}") + + +class _Context: + def __init__(self, registry: _LifecycleRegistry) -> None: + self.instance_id = "adapter" + self.registry = registry + self.services: dict[str, Any] = {} + self.logger = _Logger() + + def get(self, name: str): + if name == "hsr.registry.v1": + return self.registry + return self.services.get(name) + + def set(self, name: str, value: Any) -> None: + self.services[name] = value + + +class _Plugin(HSRAdapterPlugin): + task_catalog_factory = _Catalog + controller_factory = _Controller + task_catalog_service = "hsr.catalog.test" + controller_service = "hsr.controller.test" + display_name = "test adapter" + + +class AdapterLifecycleTests(unittest.IsolatedAsyncioTestCase): + async def test_stop_keeps_services_until_session_cleanup_can_retry(self) -> None: + registry = _LifecycleRegistry() + registry.close_results = [("SRA: busy",), ()] + context = _Context(registry) + plugin = _Plugin(context) + await plugin.on_start() + + await plugin.on_stop("reload") + + self.assertIs(context.services[plugin.task_catalog_service], plugin.catalog) + self.assertIs(context.services[plugin.controller_service], plugin.controller) + self.assertEqual(registry.owner, context.instance_id) + self.assertEqual(registry.unregister_calls, 0) + + await plugin.on_stop("retry") + + self.assertIsNone(context.services[plugin.task_catalog_service]) + self.assertIsNone(context.services[plugin.controller_service]) + self.assertIsNone(registry.owner) + self.assertEqual(registry.unregister_calls, 1) + + +@dataclass(slots=True) +class _NativeResult: + success: bool + output: str = "" + error: str = "" + returncode: int = 0 + + +class NativeResultTests(unittest.TestCase): + def test_success_preserves_native_evidence(self) -> None: + native = _NativeResult(success=True, output="done", returncode=7) + + result = HSRRunResult.from_native( + native, + default_summary="fallback", + default_error="failed", + ) + + self.assertTrue(result.success) + self.assertEqual(result.summary, "done") + self.assertEqual(result.completion_evidence, {"returncode": 7}) + self.assertIs(result.native_result, native) + + def test_failure_uses_default_without_losing_native_result(self) -> None: + native = _NativeResult(success=False, returncode=3) + + result = HSRRunResult.from_native( + native, + default_summary="fallback", + default_error="failed", + ) + + self.assertFalse(result.success) + self.assertEqual(result.error, "failed") + self.assertIs(result.native_result, native) + + +if __name__ == "__main__": + unittest.main() From 4eeae793ce0498aabe4b9f6f74b8e0efc946a59a Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Thu, 16 Jul 2026 11:50:18 +0800 Subject: [PATCH 4/4] feat(release): add automas-hsr meta-package --- .github/workflows/ci.yml | 8 +- .github/workflows/publish.yml | 4 +- AGENTS.md | 1 + README.md | 11 +- docs/release.md | 28 +- packages/automas_hsr/LICENSE | 661 ++++++++++++++++++++++++++++ packages/automas_hsr/README.md | 10 + packages/automas_hsr/pyproject.toml | 27 ++ pyproject.toml | 2 + scripts/release.py | 4 + scripts/smoke_wheels.py | 113 ++++- tests/test_package_metadata.py | 21 +- tests/test_wheel_smoke.py | 3 + uv.lock | 18 + 14 files changed, 882 insertions(+), 29 deletions(-) create mode 100644 packages/automas_hsr/LICENSE create mode 100644 packages/automas_hsr/README.md create mode 100644 packages/automas_hsr/pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f99be49..36380b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: test: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v6 with: python-version: "3.12" @@ -27,7 +27,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v6 with: python-version: "3.12" @@ -39,6 +39,10 @@ jobs: run: >- uv run python scripts/smoke_wheels.py dist --mode local-adapter-resolution + - name: Resolve the one-click meta-package against all local wheels + run: >- + uv run python scripts/smoke_wheels.py dist + --mode local-meta-resolution - uses: actions/upload-artifact@v4 with: name: distributions diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9e69da9..0b85f09 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,6 +8,7 @@ on: required: true type: choice options: + - automas-hsr - automas-script-hsr - automas-hsr-adapter-sra - automas-hsr-adapter-m7a @@ -17,6 +18,7 @@ on: type: string push: tags: + - "automas-hsr-v*" - "automas-script-hsr-v*" - "automas-hsr-adapter-sra-v*" - "automas-hsr-adapter-m7a-v*" @@ -37,7 +39,7 @@ jobs: version: ${{ steps.release.outputs.version }} artifact_name: ${{ steps.release.outputs.artifact_name }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: astral-sh/setup-uv@v6 diff --git a/AGENTS.md b/AGENTS.md index 63d47c5..dfa4620 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,7 @@ 本仓库维护 AUTO-MAS 的 HSR 插件工作区。 - `packages/` 下每个目录是独立 Python distribution;项目名、公开 entry point 和包路径不得随意变更。 +- `automas-hsr` 只负责一键安装核心包与两个引擎适配器,不包含运行时代码或插件 entry point。 - `automas-script-hsr` 是 HSR 编排和契约的唯一拥有者;SRA、三月七适配器只能依赖它,不能互相依赖。 - 插件通过稳定的 DTO 或 JSON 兼容字典跨包协作,不能将某个适配器的内部模型作为另一个包的契约。 - 代码使用 AUTO-MAS 主程序提供的 `app` 插件接口时,应保持与宿主兼容,不把宿主实现复制进本仓库。 diff --git a/README.md b/README.md index 11eb005..457ac89 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,19 @@ AUTO-MAS 的崩坏:星穹铁道插件工作区,也是 HSR 插件及其引擎适配器的唯一源码仓库。 -主程序不内置本仓库源码,也不把 HSR 脚本随主程序捆绑安装。用户只需在 AUTO-MAS -插件市场选择 SRA 或三月七助手适配器;适配器依赖会自动安装 HSR 核心包。 +主程序不内置本仓库源码,也不把 HSR 脚本随主程序捆绑安装。完整体验可一键安装 +`automas-hsr`;只使用单个引擎时,也可在 AUTO-MAS 插件市场单独选择 SRA 或 +三月七助手适配器,适配器依赖会自动安装 HSR 核心包。 ## Packages +- `automas-hsr`:一键安装 HSR 核心包与两个引擎适配器的 meta-package。 - `automas-script-hsr`:HSR 脚本入口、配置模型、调度和公共契约。 - `automas-hsr-adapter-sra`:StarRailAssistant 适配器。 - `automas-hsr-adapter-m7a`:三月七助手适配器。 -每个目录独立构建为一个 Python distribution。适配器依赖 -`automas-script-hsr`,适配器之间互不依赖。 +每个目录独立构建为一个 Python distribution。`automas-hsr` 不包含运行时代码或 +插件 entry point;适配器依赖 `automas-script-hsr`,适配器之间互不依赖。 ## Development @@ -22,6 +24,7 @@ uv run python -m unittest discover -s tests -v uv run python scripts/build_all.py uv run twine check dist/*/* uv run python scripts/smoke_wheels.py dist --mode local-adapter-resolution +uv run python scripts/smoke_wheels.py dist --mode local-meta-resolution ``` ## Publishing diff --git a/docs/release.md b/docs/release.md index b0d2f67..b81ce45 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,18 +1,21 @@ # 发布准备 -本仓库发布三个独立 distribution: +本仓库发布四个独立 distribution: +- `automas-hsr` - `automas-script-hsr` - `automas-hsr-adapter-sra` - `automas-hsr-adapter-m7a` -首次发布必须先发布核心包,再发布两个适配器。AUTO-MAS 主程序不内置这些源码; -用户只在插件市场安装自己选择的 SRA 或三月七助手适配器,适配器依赖自动拉取核心包。 +首次发布必须先发布核心包,再发布两个适配器,最后发布一键安装聚合包 +`automas-hsr`。AUTO-MAS 主程序不内置这些源码;用户既可安装聚合包获得完整双引擎 +体验,也可在插件市场只安装自己选择的适配器,适配器依赖会自动拉取核心包。 ## GitHub Environments -在 `AUTO-MAS-Project/automas-hsr` 创建三个 environment: +在 `AUTO-MAS-Project/automas-hsr` 创建四个 environment: +- `pypi-hsr` - `pypi-script-hsr` - `pypi-adapter-sra` - `pypi-adapter-m7a` @@ -27,6 +30,7 @@ PyPI 用户名、密码或长期 token。 | PyPI 项目 | Owner | Repository | Workflow | Environment | | --- | --- | --- | --- | --- | +| `automas-hsr` | `AUTO-MAS-Project` | `automas-hsr` | `publish.yml` | `pypi-hsr` | | `automas-script-hsr` | `AUTO-MAS-Project` | `automas-hsr` | `publish.yml` | `pypi-script-hsr` | | `automas-hsr-adapter-sra` | `AUTO-MAS-Project` | `automas-hsr` | `publish.yml` | `pypi-adapter-sra` | | `automas-hsr-adapter-m7a` | `AUTO-MAS-Project` | `automas-hsr` | `publish.yml` | `pypi-adapter-m7a` | @@ -41,23 +45,27 @@ environment 名必须与表格完全一致。 ## 首次发布 -1. 确认 `main` 上的 CI 全部通过,且三个 `pyproject.toml` 的版本正确。 +1. 确认 `main` 上的 CI 全部通过,且四个 `pyproject.toml` 的版本正确。 2. 在 GitHub Actions 手动运行 `Publish`,ref 必须选择 `main`。 3. 选择包并填写与该包 `pyproject.toml` 完全一致的版本号。 -4. 先发布 `automas-script-hsr`;发布成功后再依次发布两个适配器。 +4. 先发布 `automas-script-hsr`;发布成功后依次发布两个适配器,最后发布 + `automas-hsr`。 5. 每次都在对应 environment 审批后,才会通过 OIDC 上传。 全仓 CI 会分别安装 SRA/M7A adapter,并用 `--no-index` 从本次构建产物解析和安装 -`automas-script-hsr`,验证 adapter 的依赖声明确实会自动拉取 core。发布工作流会重新 -运行测试,只构建一次所选包,并执行 `twine check` 与不解析依赖的单包元数据冒烟测试; -core 按发布顺序已先存在于 PyPI。随后,同一份已验证 artifact 才交给发布 job。手动 -发布若不在 `main`,或填写版本与项目版本不一致,会在获取 PyPI OIDC 权限前失败。 +`automas-script-hsr`,验证 adapter 的依赖声明确实会自动拉取 core;还会单独安装 +`automas-hsr`,验证它能从本次构建的 wheel 一次解析 core 和两个 adapter,并暴露三个 +实际插件 entry point。发布工作流会重新运行测试,只构建一次所选包,并执行 +`twine check` 与不解析依赖的单包元数据冒烟测试;依赖包按上述发布顺序已先存在于 +PyPI。随后,同一份已验证 artifact 才交给发布 job。手动发布若不在 `main`,或填写 +版本与项目版本不一致,会在获取 PyPI OIDC 权限前失败。 ## 后续 tag 发布 后续版本使用包级 tag,tag 中版本必须与对应 `pyproject.toml` 一致: ```text +automas-hsr-v0.1.1 automas-script-hsr-v0.1.1 automas-hsr-adapter-sra-v0.1.1 automas-hsr-adapter-m7a-v0.1.1 diff --git a/packages/automas_hsr/LICENSE b/packages/automas_hsr/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/packages/automas_hsr/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 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 GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + 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 +them 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 Affero General Public License as published by + the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/automas_hsr/README.md b/packages/automas_hsr/README.md new file mode 100644 index 0000000..1a6f1a8 --- /dev/null +++ b/packages/automas_hsr/README.md @@ -0,0 +1,10 @@ +# automas-hsr + +AUTO-MAS 的 HSR 一键安装组合包。安装本包会同时安装: + +- `automas-script-hsr` +- `automas-hsr-adapter-sra` +- `automas-hsr-adapter-m7a` + +本包不包含运行时代码或插件 entry point;三个实际插件仍由各自 distribution 独立 +发现、启停和发布。 diff --git a/packages/automas_hsr/pyproject.toml b/packages/automas_hsr/pyproject.toml new file mode 100644 index 0000000..3c96c56 --- /dev/null +++ b/packages/automas_hsr/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "automas-hsr" +version = "0.1.0" +description = "AUTO-MAS HSR plugin bundle for one-click core, SRA, and M7A installation" +readme = { file = "README.md", content-type = "text/markdown" } +requires-python = ">=3.12" +authors = [{ name = "AUTO-MAS Team" }] +license = "AGPL-3.0-or-later" +license-files = ["LICENSE"] +dependencies = [ + "automas-script-hsr>=0.1.0,<0.2.0", + "automas-hsr-adapter-sra>=0.1.0,<0.2.0", + "automas-hsr-adapter-m7a>=0.1.0,<0.2.0", +] + +[project.urls] +Homepage = "https://github.com/AUTO-MAS-Project/automas-hsr" +Repository = "https://github.com/AUTO-MAS-Project/automas-hsr" +Documentation = "https://doc.auto-mas.top/" +Issues = "https://github.com/AUTO-MAS-Project/automas-hsr/issues" + +[tool.setuptools] +packages = [] diff --git a/pyproject.toml b/pyproject.toml index b1552b9..bd69b06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,12 +10,14 @@ package = false [tool.uv.workspace] members = [ + "packages/automas_hsr", "packages/automas_script_hsr", "packages/automas_hsr_adapter_sra", "packages/automas_hsr_adapter_m7a", ] [tool.uv.sources] +"automas-hsr" = { workspace = true } "automas-script-hsr" = { workspace = true } "automas-hsr-adapter-sra" = { workspace = true } "automas-hsr-adapter-m7a" = { workspace = true } diff --git a/scripts/release.py b/scripts/release.py index cd9631a..2e3410a 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -9,6 +9,10 @@ ROOT = Path(__file__).resolve().parents[1] PACKAGE_CONFIG = { + "automas-hsr": ( + "packages/automas_hsr", + "pypi-hsr", + ), "automas-script-hsr": ( "packages/automas_script_hsr", "pypi-script-hsr", diff --git a/scripts/smoke_wheels.py b/scripts/smoke_wheels.py index 5c39607..3ecf517 100644 --- a/scripts/smoke_wheels.py +++ b/scripts/smoke_wheels.py @@ -15,12 +15,16 @@ CORE_DISTRIBUTION = "automas-script-hsr" +META_DISTRIBUTION = "automas-hsr" ADAPTER_DISTRIBUTIONS = ( "automas-hsr-adapter-sra", "automas-hsr-adapter-m7a", ) -WORKSPACE_DISTRIBUTIONS = frozenset({CORE_DISTRIBUTION, *ADAPTER_DISTRIBUTIONS}) +WORKSPACE_DISTRIBUTIONS = frozenset( + {META_DISTRIBUTION, CORE_DISTRIBUTION, *ADAPTER_DISTRIBUTIONS} +) ENTRY_POINTS = { + META_DISTRIBUTION: None, CORE_DISTRIBUTION: ( "automas_script_hsr", "automas_script_hsr.plugin:Plugin", @@ -34,7 +38,11 @@ "automas_hsr_adapter_m7a.plugin:Plugin", ), } -SMOKE_MODES = ("local-adapter-resolution", "metadata-only") +SMOKE_MODES = ( + "local-adapter-resolution", + "local-meta-resolution", + "metadata-only", +) _REQUIREMENT_NAME_PATTERN = re.compile(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)") @@ -120,17 +128,22 @@ def _expected_installation( ) -> dict[str, dict[str, str]]: expected: dict[str, dict[str, str]] = {} for distribution in distributions: - entry_name, entry_value = ENTRY_POINTS[distribution] metadata = _read_wheel_metadata(wheels[distribution]) if _canonical_name(metadata.name) != distribution: raise RuntimeError( f"wheel name mismatch for {distribution}: {metadata.name!r}" ) - expected[distribution] = { - "entry_name": entry_name, - "entry_value": entry_value, - "version": metadata.version, - } + contract = {"version": metadata.version} + entry_point = ENTRY_POINTS[distribution] + if entry_point is not None: + entry_name, entry_value = entry_point + contract.update( + { + "entry_name": entry_name, + "entry_value": entry_value, + } + ) + expected[distribution] = contract return expected @@ -151,6 +164,16 @@ def _inspect_installation( raise SystemExit( f"unexpected version for {distribution}: {metadata.version!r}" ) + if "entry_name" not in contract: + plugin_entry_points = [ + item for item in metadata.entry_points if item.group == "auto_mas.plugins" + ] + if plugin_entry_points: + raise SystemExit( + f"meta-package unexpectedly declares plugin entry points: " + f"{plugin_entry_points!r}" + ) + continue entry_points = { item.name: item.value for item in metadata.entry_points @@ -267,6 +290,69 @@ def smoke_local_adapter_resolution( _inspect_installation(python, expected) +def smoke_local_meta_resolution(dist_dir: Path) -> None: + distributions = ( + META_DISTRIBUTION, + CORE_DISTRIBUTION, + *ADAPTER_DISTRIBUTIONS, + ) + wheels = { + distribution: _find_wheel(dist_dir, distribution) + for distribution in distributions + } + metadata = [ + _read_wheel_metadata(wheels[distribution]) + for distribution in distributions + ] + external_requirements = _external_requirements(metadata) + + with tempfile.TemporaryDirectory(prefix="automas-hsr-meta-smoke-") as temp_dir: + temp_root = Path(temp_dir) + dependency_wheelhouse = temp_root / "external-wheelhouse" + dependency_wheelhouse.mkdir() + if external_requirements: + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "download", + "--disable-pip-version-check", + "--only-binary=:all:", + "--dest", + str(dependency_wheelhouse), + *external_requirements, + ], + check=True, + ) + + python = _create_environment(temp_root) + local_wheelhouses = sorted({wheel.parent for wheel in wheels.values()}) + find_links = [ + item + for wheelhouse in (dependency_wheelhouse, *local_wheelhouses) + for item in ("--find-links", str(wheelhouse)) + ] + subprocess.run( + [ + str(python), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-index", + *find_links, + str(wheels[META_DISTRIBUTION]), + ], + check=True, + ) + subprocess.run([str(python), "-m", "pip", "check"], check=True) + _inspect_installation( + python, + _expected_installation(wheels, distributions), + ) + + def main() -> None: parser = argparse.ArgumentParser(description="Install and inspect built wheels") parser.add_argument("dist_dir", type=Path) @@ -286,6 +372,17 @@ def main() -> None: print(f"metadata-only wheel smoke passed: {', '.join(distributions)}") return + if args.mode == "local-meta-resolution": + if args.expected_packages: + parser.error("local-meta-resolution does not accept --expected-package") + smoke_local_meta_resolution(dist_dir) + print( + "local meta-package dependency resolution passed: " + f"{META_DISTRIBUTION} -> " + f"{CORE_DISTRIBUTION}, {', '.join(ADAPTER_DISTRIBUTIONS)}" + ) + return + adapters = tuple(args.expected_packages or ADAPTER_DISTRIBUTIONS) smoke_local_adapter_resolution(dist_dir, adapters) print( diff --git a/tests/test_package_metadata.py b/tests/test_package_metadata.py index 0949223..e46d7c4 100644 --- a/tests/test_package_metadata.py +++ b/tests/test_package_metadata.py @@ -7,6 +7,15 @@ ROOT = Path(__file__).resolve().parents[1] PACKAGES = { + "automas_hsr": { + "name": "automas-hsr", + "entry_point": None, + "dependencies": { + "automas-script-hsr>=0.1.0,<0.2.0", + "automas-hsr-adapter-sra>=0.1.0,<0.2.0", + "automas-hsr-adapter-m7a>=0.1.0,<0.2.0", + }, + }, "automas_script_hsr": { "name": "automas-script-hsr", "entry_point": "automas_script_hsr.plugin:Plugin", @@ -33,10 +42,14 @@ def test_package_metadata_and_entry_points(self) -> None: data = tomllib.loads(pyproject.read_text(encoding="utf-8")) project = data["project"] self.assertEqual(project["name"], expected["name"]) - self.assertEqual( - project["entry-points"]["auto_mas.plugins"][directory], - expected["entry_point"], - ) + if expected["entry_point"] is None: + self.assertNotIn("entry-points", project) + self.assertEqual(data["tool"]["setuptools"]["packages"], []) + else: + self.assertEqual( + project["entry-points"]["auto_mas.plugins"][directory], + expected["entry_point"], + ) self.assertTrue( expected["dependencies"].issubset(set(project.get("dependencies", []))) ) diff --git a/tests/test_wheel_smoke.py b/tests/test_wheel_smoke.py index 7089e19..bae28aa 100644 --- a/tests/test_wheel_smoke.py +++ b/tests/test_wheel_smoke.py @@ -8,6 +8,7 @@ from scripts.smoke_wheels import ( ADAPTER_DISTRIBUTIONS, CORE_DISTRIBUTION, + META_DISTRIBUTION, WheelMetadata, _external_requirements, _read_wheel_metadata, @@ -70,9 +71,11 @@ def test_ci_and_publish_use_distinct_smoke_modes(self) -> None: encoding="utf-8" ) self.assertIn("--mode local-adapter-resolution", ci) + self.assertIn("--mode local-meta-resolution", ci) self.assertIn("--mode metadata-only", publish) self.assertEqual( ADAPTER_DISTRIBUTIONS, ("automas-hsr-adapter-sra", "automas-hsr-adapter-m7a"), ) self.assertEqual(CORE_DISTRIBUTION, "automas-script-hsr") + self.assertEqual(META_DISTRIBUTION, "automas-hsr") diff --git a/uv.lock b/uv.lock index ba39ff7..3f0819f 100644 --- a/uv.lock +++ b/uv.lock @@ -4,6 +4,7 @@ requires-python = ">=3.12" [manifest] members = [ + "automas-hsr", "automas-hsr-adapter-m7a", "automas-hsr-adapter-sra", "automas-script-hsr", @@ -25,6 +26,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "automas-hsr" +version = "0.1.0" +source = { editable = "packages/automas_hsr" } +dependencies = [ + { name = "automas-hsr-adapter-m7a" }, + { name = "automas-hsr-adapter-sra" }, + { name = "automas-script-hsr" }, +] + +[package.metadata] +requires-dist = [ + { name = "automas-hsr-adapter-m7a", editable = "packages/automas_hsr_adapter_m7a" }, + { name = "automas-hsr-adapter-sra", editable = "packages/automas_hsr_adapter_sra" }, + { name = "automas-script-hsr", editable = "packages/automas_script_hsr" }, +] + [[package]] name = "automas-hsr-adapter-m7a" version = "0.1.0"